StreamPDFStreamPDF
Back to blog

PDF.js Source Walkthrough: pdf_manager

October 27, 2026 · StreamPDF Team

PDF.js Source Walkthrough: pdf_manager

Introduction

src/core/pdf_manager.js is only 244 lines long, but it decides where the bytes come from inside the worker. PDFDocument, XRef, Parser and Catalog all recognize exactly one thing: a Stream that can be read synchronously. The file may come from an in-memory ArrayBuffer, or from network responses fetched by range; the manager's job is to flatten both sources into the same Stream, then give the layers above a uniform asynchronous access interface.

The abstraction solves a structural tension:

LayerDesired readsReality
XRef / ParserSynchronous, random accessNetwork chunks may not have arrived
Network transportAsynchronous, on demandOnly one slice at a time, with latency
PDFDocumentDoes not care about the sourceMust support both in-memory and streaming

BasePdfManager wraps missing bytes as MissingDataException, wraps filling them as requestRange, and joins the two with ensure. What the layer above sees is still an ordinary property read or method call; retrying, waiting and chunking all happen inside the manager.

This article follows the real implementation of the current version, including one place where the interface differs from older versions: the file no longer forwards members such as numPages, pdfInfo and getData, and the abstract interface has shrunk to the few methods that genuinely need polymorphism.

Module Overview

The file has three parts: a URL helper, a base class and two subclasses.

ElementCategoryResponsibility
parseDocBaseUrlModule functionValidate and normalize the document base URL
BasePdfManagerAbstract base classHold options, provide shared delegation, declare the methods that must be implemented
LocalPdfManagerSubclassHolds an in-memory Stream directly
NetworkPdfManagerSubclassHolds a ChunkedStreamManager and a ChunkedStream

The base class members split into four groups by who implements them:

GroupMembersNotes
Read-only propertiesdocId, password, docBaseUrlDocument identity and security information
Shared delegationensureDoc, ensureCatalog, initDocument, getPage, fontFallback, cleanupForwarded straight to PDFDocument or Catalog
Concrete implementationupdatePasswordUpdates the password and syncs it to the encryptor
Abstract methodsensure, requestRange, requestLoadedStream, sendProgressiveData, terminateSubclasses must override them, otherwise unreachable fires

The difference between the two subclasses fits in one table:

DimensionLocalPdfManagerNetworkPdfManager
Data sourceA complete byte arrayRange requests or progressive chunks
Underlying streamStreamChunkedStream
ensure with missing bytesCannot happenCatches the exception, requests the range, retries
requestRangeNot implementedDelegates to ChunkedStreamManager
requestLoadedStreamReturns an already resolved promiserequestAllChunks
sendProgressiveDataNot implementedonReceiveData
terminateEmptyAborts the chunk manager

The worker picks one in getPdfManager based on transport capabilities:

ConditionChoiceStream type
The caller passed complete dataLocalPdfManagerStream
Response headers allow range requestsNetworkPdfManagerChunkedStream
No range support, read while downloadingBuffer first, then LocalPdfManager after the downloadStream
Range support and a streaming download in progressNetworkPdfManager with sendProgressiveDataChunkedStream

parseDocBaseUrl

docBaseUrl is the base for resolving relative URIs inside the document. It comes from the options the caller passes, but it has to pass validation before it reaches the document object.

function parseDocBaseUrl(url) {
  if (url) {
    const absoluteUrl = createValidAbsoluteUrl(url);
    if (absoluteUrl) {
      return absoluteUrl.href;
    }
    warn(`Invalid absolute docBaseUrl: "${url}".`);
  }
  return null;
}

The behaviour reduces to four kinds of input:

InputHandlingResult
Null or an empty stringReturn immediatelynull
A valid absolute URLcreateValidAbsoluteUrl parses it and validates the schemeThe normalized href
A relative pathNo base to resolve against, parsing failsWarning, null
A disallowed schemeScheme validation failsWarning, null

It accepts absolute URLs only, and never joins a relative path onto the current page address. That is deliberate: the worker runs on its own thread with no document.baseURI, so resolving relative paths stays the main thread's responsibility, completed by the caller before loading starts.

An invalid value does not throw; it logs a warning and degrades to null. The document still opens, and relative URIs encountered later simply cannot be resolved. For a purely presentational option, that degradation is more reasonable than aborting the load.

The value ultimately appears on Catalog.baseUrl:

PrioritySource
1/Base in the catalog dictionary's /URI, and it must be a valid absolute URL
2pdfManager.docBaseUrl
3null

Catalog uses this base to resolve relative links in action dictionaries, attachments and annotations; the annotation layer receives the same baseUrl. So parseDocBaseUrl, ten lines long, is the starting point of the whole URL resolution chain.

Design note: External input is normalized before it enters the core layer. docBaseUrl is parsed once, at manager construction; every later reader gets either a validated href or null, and no one has to re-check validity before each use.

Construction: One Base Class, Two Implementations

The constructor

The base constructor does three things: store the shared options, run the feature checks once, and distribute the configuration to the module-level option containers.

  constructor({
    // source,
    // disableAutoFetch,
    docBaseUrl,
    docId,
    enableXfa,
    evaluatorOptions,
    handler,
    // length,
    password,
    // rangeChunkSize,
  }) {
    if (
      (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
      this.constructor === BasePdfManager
    ) {
      unreachable("Cannot initialize BasePdfManager.");
    }
    this._docBaseUrl = parseDocBaseUrl(docBaseUrl);
    this._docId = docId;
    this._password = password;
    this.enableXfa = enableXfa;

The commented-out source, disableAutoFetch, length and rangeChunkSize parameters belong to the network implementation only, and the base class deliberately does not accept them. Declaring only the fields the base class itself uses lets both subclasses share one argument object without the base class knowing anything about transport details.

The instantiation guard only takes effect in test builds. In production builds PDFJSDev is replaced by a constant and the check is statically eliminated; in tests, an accidental new BasePdfManager(...) fails immediately with "cannot initialize the base class".

docId participates in global ID generation. PDFDocument has a global ID factory that uses g_${docId} as the document prefix, appends a font index and produces a font identity shared across pages. That identity travels all the way to the main thread and is used to reuse an already created FontFace.

password is the initial password, and may later be replaced by updatePassword. It plays no part in stream selection and affects only XRef decryption.

// Check `OffscreenCanvas` and `ImageDecoder` support once,
// rather than repeatedly throughout the worker-thread code.
evaluatorOptions.isOffscreenCanvasSupported &&= FeatureTest.isOffscreenCanvasSupported;
evaluatorOptions.isImageDecoderSupported &&= FeatureTest.isImageDecoderSupported;
 
this.evaluatorOptions = Object.freeze(evaluatorOptions);

The two &&= operators combine caller intent with runtime capability:

OptionCombined withEffect
isOffscreenCanvasSupportedFeatureTest.isOffscreenCanvasSupportedEnabled only when both are true
isImageDecoderSupportedFeatureTest.isImageDecoderSupportedEnabled only when both are true

This code modifies the object it was passed. The evaluatorOptions the caller hands in are mutable configuration until they reach the manager; inside, they are patched in place and immediately frozen with Object.freeze. The freeze is shallow: top-level properties can no longer change, but nested objects remain mutable. The real constraint is by convention: later code only reads.

The options are then distributed to eight module-level containers:

ModuleOptions receivedPurpose
ImageResizerevaluatorOptionsImage scaling thresholds
JpegStreamevaluatorOptionsJPEG decoding parameters
OperatorListevaluatorOptionsOperator list caching strategy
IccColorSpaceOptions plus handlerReads wasmUrl, useWasm, useWorkerFetch
CmykICCBasedCSOptions plus handlerReads iccUrl
PDFFunctionFactoryOptions plus handlerReads useWasm
PatternOptions plus handlerReads hasGPU
WasmImageOptions plus handlerWASM image decoding

The last five receive the option object with handler, but only WasmImage really uses it: it needs cross-thread messaging to load a WASM module inside the worker. The others only read their own URL and switch fields, and an extra handler does no harm.

These setOptions calls are module-level static methods and should run only once per worker. PDF.js's default deployment is one worker per document, so documents cannot overwrite each other's options; if a single worker opened several documents, the later options would overwrite the earlier ones, which is a known boundary of these static containers.

Design note: Feature detection runs once, options are injected once, and then the object is frozen. Inside the worker PDF.js deliberately avoids checking capabilities on every parse, compressing those global decisions into the document construction phase.

Delegation: ensureDoc and ensureCatalog

The base class does not expose pdfDocument details directly; it offers two clearly named entries, one for document properties and one for catalog properties.

ensureDoc

  ensureDoc(prop, args) {
    return this.ensure(this.pdfDocument, prop, args);
  }
 
  ensureCatalog(prop, args) {
    return this.ensure(this.pdfDocument.catalog, prop, args);
  }

Both methods are one-line forwards; only the target object differs. ensureDoc("numPages") reads a getter on PDFDocument; ensureCatalog("numPages") reads a getter on Catalog. The same name may exist on both objects with different meanings: the document layer considers linearization and XFA, while the catalog layer only knows the page tree.

this.pdfDocument.catalog inside ensureCatalog is a plain property access. Catalog is created by parse, and initDocument guarantees that parse has already finished, so the catalog object is guaranteed to exist here; what can really be missing is the bytes for later method calls, and those happen inside the try block of ensure.

Argument passing is uniform too: if the property value is a function, args is passed as its argument array; otherwise args is ignored and the property value is returned.

initDocument

The five document initialization steps all run through ensureDoc:

  async initDocument(recoveryMode) {
    await this.ensureDoc("checkHeader");
    await this.ensureDoc("parseStartXRef");
    await this.ensureDoc("parse", [recoveryMode]);
 
    // Check that at least the first page can be successfully loaded,
    // since otherwise the XRef table is definitely not valid.
    await this.ensureDoc("checkFirstPage", [recoveryMode]);
    // Check that the last page can be successfully loaded, to ensure that
    // `numPages` is correct, and fallback to walking the entire /Pages-tree.
    await this.ensureDoc("checkLastPage", [recoveryMode]);
  }

The order is not arbitrary:

StepPurposeFailure means
checkHeaderValidate the file header and versionNot a valid PDF
parseStartXRefLocate the start of the cross-reference tableThe trailing structure is damaged
parseBuild the XRef, handle object streams and encryptionThe cross-reference is unusable
checkFirstPageAt least the first page can be retrievedThe page tree root is unusable
checkLastPageVerify the page count, falling back to walking the whole page tree if neededThe page count declaration is wrong

Every step can fail because of missing bytes, and every step can change internal state. ensure retries inside a single step and never restarts initialization from the beginning; only an exception other than a missing-data one interrupts it.

recoveryMode is passed down to parse, checkFirstPage and checkLastPage. The worker initializes with false, and if it gets an XRefParseException it waits for requestLoadedStream to finish and tries again with true. The second pass has the complete bytes and can repair more aggressively, for example rebuilding the cross-reference object by object.

The Abstract Methods

Five methods in the base class call unreachable directly; they are the contract subclasses must fulfil:

MethodWhy a subclass must implement itLocalPdfManagerNetworkPdfManager
ensureDecides whether to wait and retry on missing bytesReads properties directlyCatches and requests the range
requestRangeDecides how a range becomes a network requestNever calledDelegates to the chunk manager
requestLoadedStreamDecides how to get the complete bytesAn already resolved promiseWaits for all chunks
sendProgressiveDataDecides how progressively arriving chunks are handledNever calledWrites into the chunked stream
terminateDecides how to abort a transfer in progressEmptyAborts the chunk manager

unreachable throws a clear error in test builds and is optimized into an unreachable marker in production builds. It is safer than returning undefined: if a subclass forgets to implement something, the call site fails immediately instead of continuing with an empty value.

updatePassword is the only action-shaped method with a concrete implementation in the base class:

  updatePassword(password) {
    this._password = password;
    this.pdfDocument.xref.encrypt?.setPassword(password);
  }

It updates two places at once: the manager's own _password for later reads, and the encryptor on XRef. Optional chaining is used because an unencrypted document has no encrypt, and then only the field needs updating.

cleanup, getPage and fontFallback are pure forwards too; they do not involve the byte source, so subclasses need not repeat them.

Design note: The base class abstracts only the methods related to the data source. Behaviour unrelated to transport, such as page retrieval, font fallback and cache cleanup, is implemented once through delegation, so the two subclasses do not each carry an identical copy.

LocalPdfManager: Everything in Memory

The ensure method

class LocalPdfManager extends BasePdfManager {
  constructor(args) {
    super(args);
 
    const stream = new Stream(args.source);
    this.pdfDocument = new PDFDocument(this, stream);
    this._loadedStreamPromise = Promise.resolve(stream);
  }
 
  async ensure(obj, prop, args) {
    const value = obj[prop];
    if (typeof value === 'function') {
      return value.apply(obj, args);
    }
    return value;
  }
 
  requestLoadedStream(noFetch = false) {
    return this._loadedStreamPromise;
  }
 
  terminate(reason) {}
}

The implementation is so short it barely looks like a manager, because there is nothing asynchronous in the in-memory case:

  1. args.source is a complete byte array, and new Stream wraps it into a randomly accessible stream.
  2. PDFDocument receives the manager and the stream at construction, and all later parsing runs synchronously over that memory.
  3. _loadedStreamPromise resolves immediately, so no caller waiting for complete bytes ever blocks.

ensure has no try/catch because Stream never throws MissingDataException. An out-of-range read throws a range error, which is genuine document corruption and should be visible to the layer above rather than retried.

One easily missed detail: an async function flattens its return value anyway. If the property value is itself a promise, for example numPages on an XFA document, return value still gives the caller the promise's final value. That is why ensure is declared async even though the in-memory implementation awaits nothing.

terminate is empty. There is no network request to abort and no chunk manager to close; when the instance is discarded the memory is released by the garbage collector.

CharacteristicIn-memory behaviour
First-read latencyNone
Missing-byte exceptionsNever happen
Retrieving complete bytesImmediate
Abort costNone
Supported document sizeLimited by memory

NetworkPdfManager: Filling Chunks on Demand

The ensure method

class NetworkPdfManager extends BasePdfManager {
  constructor(args) {
    super(args);
 
    this.streamManager = new ChunkedStreamManager(args.source, {
      msgHandler: args.handler,
      length: args.length,
      disableAutoFetch: args.disableAutoFetch,
      rangeChunkSize: args.rangeChunkSize,
    });
    this.pdfDocument = new PDFDocument(this, this.streamManager.getStream());
  }
 
  async ensure(obj, prop, args) {
    try {
      const value = obj[prop];
      if (typeof value === "function") {
        return await value.apply(obj, args);
      }
      return value;
    } catch (ex) {
      if (!(ex instanceof MissingDataException)) {
        throw ex;
      }
      await this.requestRange(ex.begin, ex.end);
      return this.ensure(obj, prop, args);
    }
  }

The constructor hands four network parameters to ChunkedStreamManager: the message handler, the total file length, whether auto-prefetch is disabled, and the chunk size. The ChunkedStream returned by getStream() is a fixed-length array where loaded chunks are written in and unloaded chunks stay blank.

ensure is the most critical piece in the file. It catches exactly one exception type: MissingDataException.

Exception typeHandlingReason
MissingDataExceptionRequest the missing range, retry recursivelyThe data has simply not arrived yet, so waiting is fine
Any other exceptionRethrow as-isCorrupt document, failed decryption, syntax error

The retry path requests the range and then re-runs the same property read or method call. This means a getter wrapped by ensure must be able to use existing caches on re-entry. PDF.js relies heavily on shadow caches: the first successful computation writes the result back to the instance property, the second read hits it directly, and nothing is parsed twice. If a method restarted from scratch on every call, retries would do the same useless work over and over.

The recursion has no attempt limit. Safety comes from the other side: the promise returned by requestRange only resolves once the corresponding chunk really arrives, and if the manager is aborted it rejects, so the recursion ends naturally. As long as the requested range really covers the gap, every round makes progress.

Seen next to ObjectLoader, the division is clear:

ScenarioWho handles itRequest granularity
Document initialization, single property readsNetworkPdfManager.ensureOne missing range
Page resource batch prefetchObjectLoaderMany ranges merged

ensure is the backstop that guarantees any synchronous access eventually gets its bytes; ObjectLoader is the optimization that merges many small gaps into a few requests so that the backstop is rarely triggered.

requestRange and requestLoadedStream

  requestRange(begin, end) {
    return this.streamManager.requestRange(begin, end);
  }
 
  requestLoadedStream(noFetch = false) {
    return this.streamManager.requestAllChunks(noFetch);
  }

requestRange only forwards. How a range turns into chunk numbers, how chunks are merged, and how GetRangeReader is sent to the main thread through the message handler are all decided by ChunkedStreamManager. The manager cares about none of that; it only needs a promise for this range to arrive.

The noFetch parameter of requestLoadedStream deserves a separate note:

noFetchBehaviourUsed by
falseRequest every missing chunk and wait for all of themGetData, SaveDocument
trueSend no request, only wait for transfers already scheduledThe DataLoaded event, recovery mode

In the worker, GetData waits for this promise and then returns stream.bytes; SaveDocument waits for it first as well, because writing back needs the complete original bytes. noFetch set to true serves the case of confirming that the current download has finished, so a status notification does not trigger another request round.

sendProgressiveData and terminate

  sendProgressiveData(chunk) {
    this.streamManager.onReceiveData({ chunk });
  }
 
  terminate(reason) {
    this.streamManager.abort(reason);
  }

Progressive data is the entry point of read-while-downloading. In getPdfManager, the worker reads the network stream and hands each chunk to sendProgressiveData; if the manager is not built yet, the chunks go into a temporary array and are replayed afterwards. The object onReceiveData receives carries only chunk, no begin, because a progressive download always continues from the current write pointer and needs no explicit offset.

terminate fails all pending range requests. When the worker terminates it also calls a separate cancel function to abort the underlying XHR, but this layer only has to guarantee that promises being awaited do not hang forever.

Comparing the two implementations side by side makes the real boundary of the abstraction clear:

BehaviourIn-memory implementationNetwork implementation
Reading a propertySynchronous readSynchronous read, waits when bytes are missing
Reading a methodDirect callDirect call, retries when bytes are missing
Waiting for complete bytesAlready readyRequests the missing chunks
Receiving progressive dataNot applicableWrites into the chunked stream
AbortNo-opAborts the chunk manager and rejects pending requests

Design note: The abstract base class specifies what can be done, not how. ensure differs a lot between the two subclasses, yet is fully transparent to callers; that is precisely why PDFDocument can hold one set of parsing code and still support both in-memory and streaming sources.

Document, XRef and the Manager

The manager parses no bytes itself; it only hands out the stream. PDFDocument stores the manager and the stream at construction and creates XRef immediately:

this.pdfManager = pdfManager;
this.stream = stream;
this.xref = new XRef(stream, pdfManager);

The XRef constructor stores the same two things:

  constructor(stream, pdfManager) {
    this.stream = stream;
    this.pdfManager = pdfManager;

Together they form a fixed reference chain: PDFDocument holds xref and stream, and XRef holds stream and pdfManager. Whenever any layer needs bytes, it can follow pdfManager back to ensure or requestRange without extra global state.

Different readers go through different entries:

ReaderWhat it actually readsPath when bytes are missing
PDFDocument.checkHeaderstream.getBytesensureDoc catches and retries
PDFDocument.startXRefstream.getBytesensureDoc catches and retries
XRef.parsestream and ParserensureDoc("parse")
XRef.fetchstream.getBytesCaught by an outer ensure
ObjectLoaderxref.stream.manager.requestRangesBatches requests itself, bypassing ensure
PDFDocument.#getByteRangestream.getByteRangeCalls requestRange directly
Catalog propertiesReads XRef indirectlyensureCatalog catches and retries

In the in-memory implementation all these paths degrade to plain array reads. In the network implementation a typical read goes through six steps:

  1. The layer above calls ensureDoc("parse"), entering the try block of NetworkPdfManager.ensure.
  2. XRef.parse reads the stream, ChunkedStream finds the target chunk not loaded, and throws an exception carrying begin and end.
  3. ensure catches it, calls requestRange(begin, end) and waits.
  4. ChunkedStreamManager converts the range into chunks and sends a range request to the main thread through the message handler.
  5. The data is written into the chunked stream when it returns, and the corresponding promise resolves.
  6. ensure recursively re-runs ensureDoc("parse"); this time the read hits loaded chunks, and the result of the single getter is cached by shadow, so later access never takes this path again.

PDFDocument.#getByteRange is the exception. It exists for signature validation and reads only a short contiguous stretch; rather than throwing up to ensureDoc, it catches locally and calls requestRange directly, saving one level of re-entry:

  async #getByteRange(begin, end) {
    try {
      return this.stream.getByteRange(begin, end);
    } catch (ex) {
      if (!(ex instanceof MissingDataException)) {
        throw ex;
      }
      await this.pdfManager.requestRange(begin, end);
      return this.#getByteRange(begin, end);
    }
  }

This code has almost the same shape as NetworkPdfManager.ensure, which shows that catching missing bytes, requesting the range and retrying is the common pattern of this layer. Only the retried object differs: one re-runs a property read, the other a byte slice.

ensure has no retry count and does not need one. It relies on a stronger guarantee: the requested range covers the gap named in the exception, and once the range has arrived the same read must be able to advance. If the underlying transfer is aborted, the promise rejects and the recursion stops. Leaving the termination condition to the data layer is more reliable than guessing an upper bound in the retry layer.

Design note: Retry logic for byte reads is allowed in exactly two places: ensure, and the very few local methods that need precise slices. The more call sites there are, the greater the risk of duplicate requests and inconsistent state; once the entry points are narrowed, XRef and Catalog can stay purely synchronous.

Version Note: The Interface Boundary

Members of BasePdfManager are not identical across PDF.js versions. This version removed some forwarding getters so the manager can focus on supplying data. If the material you read mentions the names below, here is where they live in this version:

NamePresent in this versionActual location
pdfDocumentYesInstance property assigned in the subclass constructor
docIdYesBase class getter
numPagesNoPDFDocument.numPages, reached through ensureDoc
numPagesWithOffsetNoNot present in this version
pdfInfoNoThe payload of the GetDoc message, holding numPages, fingerprints and htmlForXfa
ensureYesBase class abstract method
ensureDocYesBase class delegation method
ensureCatalogYesBase class delegation method
requestRangeYesBase class abstract method, delegated to the chunk manager in the network implementation
getDataNorequestLoadedStream; the worker's GetData takes stream.bytes
requestRangesNoChunkedStreamManager.requestRanges, called by ObjectLoader
cleanupYesBase class delegates to PDFDocument.cleanup
terminateYesBase class abstract method
updateMetadataNoNot present in this version
updateDocumentNoSaving goes through SaveDocument and the writer

These differences show one thing: the abstract interface shrinks as the implementation evolves. Early versions put getters such as numPages and pdfInfo on the manager, which saved callers one level of ensureDoc, but the manager ended up knowing too much about the document's internals. Now those reads all go through ensureDoc, and the manager keeps only the polymorphic points that are tightly tied to transport.

When reading source, trust the current file over a remembered member list from some version. To decide whether a member belongs to the abstract interface, check whether it is marked with unreachable in several places, or whether both subclasses implement it differently.

Conclusion

pdf_manager.js is PDF.js's data supply layer: upward it gives PDFDocument and XRef a uniform stream and access interface, and downward it hides the differences between in-memory bytes, range requests and progressive downloads.

Where It Sits in the Overall Chain

StageModuleInputOutput
Starting a loaddisplay/api.jsFile, URL, range parametersGetDocRequest
Choosing the implementationgetPdfManager in worker.jsComplete data or range capabilityLocalPdfManager or NetworkPdfManager
Creating the streamManager constructorByte array or network streamStream or ChunkedStream
Creating the documentPDFDocumentManager and streampdfDocument
InitializationinitDocumentRecovery modeA parsed XRef and Catalog
Property accessensureDoc / ensureCatalogProperty name and argumentsA value, or one retry
Filling bytesrequestRange / requestLoadedStreamA range or everythingData arriving
Lifecyclecleanup / terminateThe trigger reasonCache release and request abort

In one sentence: the manager keeps the possibility of missing data behind closed doors, so PDFDocument and XRef can assume the bytes are always available.

Five Design Principles

  1. Dependency inversion. PDFDocument depends only on the manager's abstract interface, not on whether the data comes from memory or the network; transport differences are confined to the two subclasses.
  2. Missing bytes as control flow. MissingDataException is not an error but a signal asking for more data; only it is caught by ensure and retried.
  3. Options injected once. Feature detection, option freezing and module-level setOptions all happen during document construction, and the hot path repeats none of those checks.
  4. Two implementations, one semantics. Whether in memory or over the network, ensure promises to return the property value or throw a real error; the in-memory implementation merely reduces waiting to zero cost.
  5. Explicit lifecycle. Complete bytes are obtained through requestLoadedStream, caches are released through cleanup, and transfers are aborted through terminate; every action has a clear entry point and none relies on implicit collection.

StreamPDF design principle: anything that can be handled ahead of time is absolutely not deferred to parse time; the server solves all performance problems once. The browser only fetches and renders the data it must.

Related articles