How to Use the PDF.js API for Browser PDF Rendering
Render PDF pages in a browser with PDF.js: workers, canvases, HiDPI scaling, CORS, lazy rendering, troubleshooting, and production patterns.

PDF.js renders PDF pages in the browser by parsing a document in a worker, creating a viewport for a page, sizing an HTML canvas, and drawing the page with the display API. For most applications, use the supported display layer from the pdfjs-dist package, configure a worker built from the exact same version, and render only the pages users need.
The core API can parse PDF internals, but Mozilla describes it as advanced and subject to change. The display layer exposes the easier browser-facing API for rendering pages and reading document information. The full PDF.js viewer is another option when you want an existing interface that you can adapt. See the PDF.js Getting Started guide and official browser example for the project’s current setup details.
What the PDF.js rendering pipeline does
A browser integration has five asynchronous stages:

- Load the display module and worker. The display module runs the public API. The worker performs PDF parsing away from the main UI thread.
- Open the document.
pdfjsLib.getDocument()returns a loading task. Itspromiseresolves to a PDF document. - Get a page.
pdf.getPage(pageNumber)resolves to a page object. - Create a viewport.
page.getViewport({ scale })calculates page dimensions, scale, and rotation. - Render. Give
page.render()a canvas context and viewport, then await its render task before drawing another page on that canvas.
This sequence mirrors the promise-based flow in the official Hello World example and walkthrough. A render task should finish before its canvas is reused for a different page.
Install PDF.js and make the worker available
For a bundler application, install the distribution package:
npm install pdfjs-dist
Pin the package version in your lockfile and deploy the matching worker file. The project’s getting-started page recommends the latest official release for production; its page listed v6.3.289 when this article was researched on 2026-09-29. Recheck the project releases before publishing or upgrading, and keep the display module and worker on exactly the same version.
Vite, Webpack, and other module bundlers
One common ESM setup imports the worker as a URL:
import * as pdfjsLib from 'pdfjs-dist/build/pdf.mjs';
import pdfWorker from 'pdfjs-dist/build/pdf.worker.mjs?url';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker;
The exact import suffix depends on your bundler and the installed pdfjs-dist version. If your bundler emits the worker as a separate asset, point workerSrc at that emitted URL. The important rule is that the worker is served over HTTP and comes from the same PDF.js version as the display module.
Prebuilt browser files
The project also publishes prebuilt releases. Serve the display script and worker from your application’s static assets or another origin configured for worker loading. Do not open the page directly with a file:// URL: the getting-started documentation explains that the worker is not enabled in that mode, so use a local HTTP server during development.
Minimal browser example
This complete example loads a PDF URL and renders its first page. It assumes your bundler supports the worker URL import shown above.
import * as pdfjsLib from 'pdfjs-dist/build/pdf.mjs';
import pdfWorker from 'pdfjs-dist/build/pdf.worker.mjs?url';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker;
const pdfUrl = '/documents/guide.pdf';
const canvas = document.querySelector('#pdf-canvas');
const context = canvas.getContext('2d');
async function renderFirstPage() {
const loadingTask = pdfjsLib.getDocument({ url: pdfUrl });
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);
const scale = 1.5;
const viewport = page.getViewport({ scale });
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;
await page.render({
canvasContext: context,
viewport
}).promise;
console.log(`Rendered page 1 of ${pdf.numPages}`);
}
renderFirstPage().catch((error) => {
console.error('PDF rendering failed', error);
});
<canvas id="pdf-canvas" aria-label="PDF page"></canvas>
A scale of 1.5 is the demonstration value used by the official example, not a universal recommendation. Increase it for detail, and reduce it when memory or rendering time matters.
Sharp output on HiDPI displays
The canvas has a pixel backing store and a CSS display size. On a Retina or other HiDPI screen, multiply the backing dimensions by window.devicePixelRatio, keep the CSS dimensions at the viewport’s logical size, and pass a transform to the render task:

const cssViewport = page.getViewport({ scale: 1.5 });
const outputScale = window.devicePixelRatio || 1;
canvas.width = Math.floor(cssViewport.width * outputScale);
canvas.height = Math.floor(cssViewport.height * outputScale);
canvas.style.width = `${Math.floor(cssViewport.width)}px`;
canvas.style.height = `${Math.floor(cssViewport.height)}px`;
const transform = outputScale !== 1
? [outputScale, 0, 0, outputScale, 0, 0]
: null;
await page.render({
canvasContext: context,
viewport: cssViewport,
transform
}).promise;
Without this separation, pages can look blurry or the layout can become unexpectedly large. Very high device-pixel ratios also increase canvas memory, so cap the effective output scale when large documents cause pressure.
Loading documents from URLs or bytes
getDocument accepts a URL, typed-array data, or a parameter object. A URL is convenient when the PDF is already publicly reachable:
const loadingTask = pdfjsLib.getDocument({
url: 'https://cdn.example.com/manual.pdf'
});
For an authenticated application, fetch bytes yourself and pass them to PDF.js. This keeps authorization headers in your application request rather than exposing them in a public URL:
const response = await fetch('/api/documents/42', {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
const data = new Uint8Array(await response.arrayBuffer());
const pdf = await pdfjsLib.getDocument({ data }).promise;
PDF.js may use HTTP range requests to fetch portions of a document when browser and server response headers support it. Configure your server or CDN to handle range requests and expose the required headers through CORS when the PDF is cross-origin. If range loading is unavailable, the browser may download the complete file before rendering.
Render a selected page and build navigation
Keep the PDF document object, current page number, and one render task in application state. Render only after the previous task completes:
let pdfDocument;
let pageNumber = 1;
let activeRenderTask = null;
async function openPdf(source) {
pdfDocument = await pdfjsLib.getDocument(source).promise;
pageNumber = 1;
await showPage(pageNumber);
}
async function showPage(number) {
if (!pdfDocument) return;
if (activeRenderTask) {
activeRenderTask.cancel();
try { await activeRenderTask.promise; } catch (error) {
if (error?.name !== 'RenderingCancelledException') throw error;
}
}
const page = await pdfDocument.getPage(number);
const viewport = page.getViewport({ scale: 1.25 });
const canvas = document.querySelector('#pdf-canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
activeRenderTask = page.render({ canvasContext: context, viewport });
await activeRenderTask.promise;
activeRenderTask = null;
document.querySelector('#page-label').textContent =
`Page ${number} of ${pdfDocument.numPages}`;
}
document.querySelector('#previous').onclick = async () => {
if (pageNumber > 1) await showPage(--pageNumber);
};
document.querySelector('#next').onclick = async () => {
if (pageNumber < pdfDocument.numPages) await showPage(++pageNumber);
};
For a scrolling viewer, use an intersection observer or virtualized list to render visible pages and release canvases that are far outside the viewport. The PDF.js FAQ explains that its demo viewer creates, renders, and holds canvases only for visible pages to reduce memory use.
Useful document and page options
| Need | Approach | Trade-off |
|---|---|---|
| Rotate a page | page.getViewport({ scale, rotation: 90 }) |
Changes geometry and canvas dimensions. |
| Render a thumbnail | Use a small scale such as 0.2. |
Fast and small, but text is not readable at full size. |
| Print-quality preview | Use a larger scale and HiDPI backing dimensions. | More pixels, memory, and render time. |
| Authenticated source | Fetch an ArrayBuffer with your credentials, then pass Uint8Array data. |
Your server handles authorization and download errors. |
| Existing complete UI | Start from the official viewer. | Less custom integration work, less control over your application’s UX. |
Do not treat the internal core layer as the normal integration surface. Its API is advanced and may change; the display layer is the practical supported choice for custom browser rendering.
Cross-origin, security, and browser constraints
- CORS: A PDF on another origin must send permissive CORS headers for your application, or your backend must proxy it. Browser same-origin rules apply to PDF.js just as they do to other fetches.
- Worker origin: Serve the worker from an HTTP(S) origin that your application can load. A stale cached worker can be as problematic as a wrong URL.
- Untrusted PDFs: Treat document metadata and downloaded bytes as untrusted input. Keep your application’s authorization checks on the server and avoid putting bearer tokens in query strings.
- Large files: Avoid creating a canvas for every page at full resolution. Render on demand and discard canvases that are no longer needed.
Performance and reliability checklist
- Choose the smallest scale that meets the visual requirement.
- Use device-pixel-ratio scaling only for the output quality you need.
- Render the first visible page immediately, then schedule other visible pages.
- Cancel an in-progress render before reusing its canvas.
- Prefer range-capable hosting for large PDFs and verify CORS response headers.
- Cache the PDF bytes when the same document is opened repeatedly, but cap cache size.
- Show loading, progress, and error states around both the loading task and render task.
- Pin the display package and worker to one version, then upgrade them together.
There are no controlled performance benchmarks in the reviewed PDF.js sources. Actual speed depends on PDF structure, page complexity, scale, device pixel ratio, browser, and whether the server can provide ranges.
Troubleshooting common PDF.js errors
| Symptom | Likely cause | Fix |
|---|---|---|
| “API version … does not match Worker version …” | The worker and display package differ, often because of a stale cache. | Use the exact same pdfjs-dist version for both, update the worker URL, and clear old generated assets. |
| Worker disabled or fake worker warning | The app was opened with file://, or the worker URL cannot be loaded. |
Run an HTTP development server and verify the worker asset returns JavaScript with a 200 response. |
| Failed to fetch PDF | Wrong URL, missing credentials, server error, or CORS rejection. | Inspect the network response, check authentication, configure CORS, or proxy the document through your server. |
| Canvas is blank | The render promise failed, dimensions are zero, or a previous render was cancelled. | Await page.render(...).promise, log the rejection, set canvas dimensions from the viewport, and handle cancellation separately. |
| Blurry text | The backing store is only CSS-sized on a HiDPI display. | Multiply canvas width and height by devicePixelRatio and pass the matching transform. |
| Tab becomes unresponsive | Many high-resolution canvases were rendered simultaneously. | Render visible pages only, lower scale, cancel off-screen work, and release old canvases. |
| Only some pages load | Range requests or a server response are incomplete. | Check range support, CORS-exposed headers, content length, and proxy behavior. |
Or skip the browser setup
If your goal is a reliable image or PDF of a web page rather than an interactive in-browser PDF viewer, ScreenshotNeo provides a single screenshot API request. Its clean-shot steps accept cookie and consent banners, remove more than 60 known consent platforms plus newsletter popups and chat widgets, and each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the result with X-Page-Verdict and X-Billed headers.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request options. It supports full-page and element capture, device presets and custom viewports, dark mode, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, selectable cache TTLs, signed links, asynchronous jobs, signed webhooks, bulk capture, usage data, and an OpenAPI specification. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 shots each month without a card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Should I use the PDF.js viewer or the display API?
Use the display API when your application owns the controls and layout. Start from the full viewer when you need a mature document interface and can adapt its structure.
Can PDF.js render a PDF without a canvas?
The standard browser display flow renders page graphics to a canvas. You can build other output layers around the parsed page data, but canvas is the supported practical path for visual page rendering.
Why does changing CSS width not improve detail?
CSS changes the displayed size. Detail comes from the canvas backing dimensions and the viewport scale, with the HiDPI transform applied during rendering.
Can I render all pages at startup?
You can, but it increases memory and startup work. Render visible pages on demand and keep only the canvases needed for navigation.
Where should the PDF URL be hosted?
It may be on your application origin or a CDN that supports the required CORS and range-request behavior. Otherwise, download it through an application server and pass the bytes to PDF.js.


