Advanced PDF.js loading: Streaming, range requests, and web worker patterns
Table of contents
OffscreenCanvas for high-performance PDF applications.
- Stream large PDFs progressively from a URL and tune
rangeChunkSize/disableAutoFetchfor slow networks. - Use
PDFDataRangeTransportwhen bytes come from a custom source (encrypted storage, WebSocket, signed API). - Cancel pending
page.render()calls during fast scrolling and useOffscreenCanvasto render off the main thread.
Prerequisites
This guide assumes you’ve already wired up a PDF.js viewer — getDocument, a worker URL on GlobalWorkerOptions.workerSrc, and a PDFDocumentProxy you can render pages from. If you haven’t, start with our blog on how to set up a custom PDF.js viewer in React.
You’ll also need:
pdfjs-dist4.x or later- A server that supports HTTP
Rangeheaders (for the range-request and progressive-loading sections)
Streaming/progressive loading
PDF.js can display a document before it has fully downloaded, either by letting the browser issue range requests automatically or by supplying a custom transport for full control over how bytes arrive.
Progressive loading from a URL
getDocument doesn’t accept a ReadableStream directly — its data option only takes TypedArray | ArrayBuffer | string. To stream a PDF as it downloads, pass the URL and let PDF.js handle range requests automatically (see below), or supply a custom PDFDataRangeTransport for full control over how bytes are fetched.
import { getDocument } from "pdfjs-dist";
const loadingTask = getDocument({ url: "https://example.com/large-document.pdf",});
const pdfDocument = await loadingTask.promise;// Pages become available as data arrives.Range requests
PDF.js can request specific byte ranges of a PDF file, enabling fast initial page display, even for huge documents. This requires the server to support Range headers:
const loadingTask = getDocument({ url: "https://example.com/large-document.pdf", rangeChunkSize: 1024 * 1024, // 1MB chunks disableAutoFetch: false, // true = only fetch on demand disableStream: false, // true = disable streaming});Configuration options
| Option | Default | Description |
|---|---|---|
rangeChunkSize | 65536 (64KB) | Size of each range request chunk |
disableAutoFetch | false | If true, only fetches data when pages are actually needed |
disableStream | false | If true, disables streaming (waits for full download) |
disableRange | false | If true, disables range requests entirely |
disableAutoFetch has a dependency: It only takes effect when disableStream is false. If you turn off streaming, the rest of the file downloads regardless of disableAutoFetch.
Custom transport with PDFDataRangeTransport
Use a custom PDFDataRangeTransport for complete control over how PDF data is fetched (e.g. from a custom API, encrypted storage, or WebSocket):
import { getDocument, PDFDataRangeTransport } from "pdfjs-dist";
class CustomTransport extends PDFDataRangeTransport { constructor(length, initialData) { super(length, initialData); }
requestDataRange(begin, end) { // Fetch the byte range from your custom source. fetchBytesFromCustomSource(begin, end).then((data) => { this.onDataRange(begin, data); }); }}
// Usage.const transport = new CustomTransport(fileSize, headerBytes);const loadingTask = getDocument({ range: transport });Worker configuration
PDF.js always parses in a background worker; configure how that worker’s script loads, swap out its communication channel, or turn it off entirely for debugging.
Standard worker setup
Point GlobalWorkerOptions.workerSrc at the worker script, either as a plain URL string or resolved from node_modules:
import { GlobalWorkerOptions } from "pdfjs-dist";
// Option 1: URL string.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";
// Option 2: Resolved from node_modules.GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/legacy/build/pdf.worker.min.mjs", import.meta.url,).toString();Custom worker port (MessagePort)
You can pass any MessagePort as the worker communication channel:
const { port1, port2 } = new MessageChannel();
// Send port2 to an existing worker.existingWorker.postMessage({ pdfWorkerPort: port2 }, [port2]);
// Use port1 with PDF.js.const loadingTask = getDocument({ url: "document.pdf", workerPort: port1,});Disabling workers (debugging)
For debugging, you can run PDF.js in the main thread by passing disableWorker: true to getDocument:
const loadingTask = getDocument({ url: "document.pdf", disableWorker: true,});Only use this for debugging — it blocks the main thread during parsing.
Page render cancellation
For fast-scrolling scenarios, cancel pending renders to free resources:
let activeRenderTasks = new Map();
async function renderPage(page, canvas, viewport) { const pageNumber = page.pageNumber;
// Cancel any existing render for this page. if (activeRenderTasks.has(pageNumber)) { activeRenderTasks.get(pageNumber).cancel(); }
const renderTask = page.render({ canvasContext: canvas.getContext("2d"), viewport, });
activeRenderTasks.set(pageNumber, renderTask);
try { await renderTask.promise; activeRenderTasks.delete(pageNumber); } catch (error) { if (error.name === "RenderingCancelledException") { // Expected when canceled — do nothing. return; } throw error; }}RenderTask API
page.render() returns a RenderTask with a promise, a cancel() method, and an onContinue hook for pausing and resuming rendering:
const renderTask = page.render({ canvasContext, viewport });
renderTask.promise // Promise that resolves when rendering completes.renderTask.cancel() // Cancel the rendering.renderTask.onContinue = (cont) => { // Called during rendering — allows you to pause/resume. cont(); // Call to continue rendering.};Custom canvas factory
Override how canvases are created for rendering:
const loadingTask = getDocument({ url: "document.pdf", canvasFactory: { create(width, height) { const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; return canvas; }, reset(canvas, width, height) { canvas.width = width; canvas.height = height; }, destroy(canvas) { canvas.width = 0; canvas.height = 0; }, },});OffscreenCanvas for worker-based rendering
Render in a web worker (off the main thread) with OffscreenCanvas:
// In a web worker:const offscreenCanvas = new OffscreenCanvas(width, height);const context = offscreenCanvas.getContext("2d");
await page.render({ canvasContext: context, viewport,}).promise;
// Transfer the bitmap back to main thread.const bitmap = offscreenCanvas.transferToImageBitmap();self.postMessage({ bitmap }, [bitmap]);Loading task events
Monitor document loading progress:
const loadingTask = getDocument({ url: "large-document.pdf" });
loadingTask.onProgress = (progress) => { if (!progress.total) return; // Server may omit `Content-Length`. const percent = (progress.loaded / progress.total) * 100; console.log(`Loading: ${percent.toFixed(1)}%`); updateProgressBar(percent);};
loadingTask.onPassword = (updateCallback, reason) => { // PDF is password-protected. const password = prompt("Enter PDF password:"); updateCallback(password);};Password-protected PDFs
Check reason against PasswordResponses to tell an initial password prompt apart from a retry after an incorrect one:
import { PasswordResponses } from "pdfjs-dist";
const loadingTask = getDocument({ url: "protected.pdf" });
loadingTask.onPassword = (callback, reason) => { if (reason === PasswordResponses.NEED_PASSWORD) { const password = prompt("This PDF is password-protected. Enter password:"); callback(password); } else if (reason === PasswordResponses.INCORRECT_PASSWORD) { const password = prompt("Wrong password. Try again:"); callback(password); }};Key points
rangeChunkSizecontrols how much data PDF.js requests at once — increase for fast networks, and decrease for slow ones.disableAutoFetch: trueonly downloads page data when that page is scrolled to (saves bandwidth).PDFDataRangeTransportgives complete control over how PDF bytes are fetched.renderTask.cancel()is essential for smooth fast-scrolling performance.loadingTask.onProgressenables loading progress bars.loadingTask.onPasswordhandles password-protected PDFs.- Use
OffscreenCanvasfor off-main-thread rendering in performance-critical apps.
How Nutrient Web SDK handles this
Nutrient Web SDK ships with progressive loading, range-request handling, render prioritization, and memory management configured by default. The viewer initializes with a single NutrientViewer.load() call:
const instance = await NutrientViewer.load({ container: "#pdf-container", document: "document.pdf",});The viewer applies its own loading strategy — there’s no GlobalWorkerOptions.workerSrc to set, no PDFDataRangeTransport to subclass, and no separate OffscreenCanvas plumbing to maintain. If you need to tune behavior (caching, prefetch, network policy), it’s a configuration option rather than a custom transport.
FAQ
ReadableStream to getDocument?No. The data option only accepts TypedArray | ArrayBuffer | string. To stream bytes as they arrive, pass the URL (PDF.js will use HTTP Range requests automatically when the server supports them) or implement a custom PDFDataRangeTransport.
disableAutoFetch actually do?With disableAutoFetch: true, PDF.js stops downloading the rest of the file once the initial range needed to display the current page has arrived. Pages that haven’t been scrolled to yet are fetched on demand. Use it on slow networks or when the user might not read the whole document — at the cost of a longer wait when they finally scroll.
Cancel pending renders whenever the page they’re producing is no longer needed — scrolling past a page, zooming (which forces a fresh viewport), or rotating. Without cancellation, fast scrolling queues up renders that block the worker and freeze the UI. Catch RenderingCancelledException on the rejected renderTask.promise and treat it as a no-op.
OffscreenCanvas?Only if main-thread rendering is a measurable bottleneck for your app. PDF.js’s worker already parses PDFs off the main thread; OffscreenCanvas adds the rasterization step to that. The cost is shipping each frame back as an ImageBitmap via postMessage, which can outweigh the savings for typical viewers. Profile first.
Range requests?PDF.js downloads the entire file in one request. It then parses it once the download completes. You can force this behavior with disableRange: true. Without Range support, you also lose the ability to display the first page before the rest of the document has arrived.
Yes — pass disableWorker: true to getDocument. The library will run parsing on the main thread, which blocks the UI during loading and rendering. Use it only for debugging or for environments (like some test runners) where web workers aren’t available.
See Nutrient Web SDK for streaming and progressive loading handled by the viewer, or follow the migration guide to switch from PDF.js. Talk to Sales about your requirements.