DEV Community

Cover image for How to Handle PDF Rendering in React Without Killing Performance
Simon Briggs
Simon Briggs

Posted on

How to Handle PDF Rendering in React Without Killing Performance

You add a PDF viewer to your React app, test it with a two-page invoice, and everything feels instant. Then a customer uploads a 300-page scanned contract. The tab freezes, the fan spins up, and your support inbox fills with "the viewer is broken" tickets.

PDF rendering is one of those features that looks trivial in a demo and turns expensive in production. The good news is that most of the pain comes from a handful of avoidable mistakes. This guide walks through them in the order you are likely to hit them.

Why PDFs Are Heavy to Render

A PDF is not a document you can simply paint. It is a set of drawing instructions: fonts, vector paths, embedded images, and layout operators. Libraries like PDF.js, which powers react-pdf, must parse those instructions and rasterize each page onto a <canvas>. A dense page with large embedded images can take real time and memory to draw.

Multiply that by hundreds of pages, plus a high-DPI display, and you can see how a naive "render everything" approach falls over. So the strategy is simple: do less work, do it later, and do it off the main thread.

1. Set Up the Worker Correctly

PDF.js parses documents in a Web Worker so the UI thread stays responsive. If the worker is misconfigured, the library may fall back to a slower path or throw errors. With react-pdf on a modern bundler, the setup looks like this:

import { pdfjs } from 'react-pdf';

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
 'pdfjs-dist/build/pdf.worker.min.mjs',
 import.meta.url
).toString();
Enter fullscreen mode Exit fullscreen mode

Run this once at module level, not inside a component. Also make sure the worker version matches the installed pdfjs-dist version, since a mismatch is a common source of confusing runtime errors.

2. Keep Your Props Referentially Stable

This is the most common React-specific mistake. If you pass a new options object or a new file object on every render, the Document component treats it as a different input and reloads the entire PDF.

// Bad: new object every render
<Document file={url} options={{ cMapUrl: '/cmaps/' }} />

// Good: defined once, outside the component
const options = { cMapUrl: '/cmaps/', standardFontDataUrl: '/standard_fonts/' };

function Viewer({ url }) {
 return <Document file={url} options={options} />;
}
Enter fullscreen mode Exit fullscreen mode

If you pass a File or Blob, keep it in state or a ref. If the value must be computed, wrap it in useMemo. Your console will often warn you about this, so do not ignore that warning.

3. Render Only What the User Can See

Rendering all pages up front is the single biggest performance killer. A user looking at page 4 does not need page 240 painted on a canvas. Use a virtualization library such as react-window so only visible pages exist in the DOM:

import { useState } from 'react';
import { FixedSizeList } from 'react-window';
import { Document, Page } from 'react-pdf';

function Viewer({ file }) {
 const [numPages, setNumPages] = useState(0);

 return (
   <Document file={file} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
     <FixedSizeList height={800} width={620} itemCount={numPages} itemSize={820}>
       {({ index, style }) => (
         <div style={style}>
           <Page pageNumber={index + 1} width={600} />
         </div>
       )}
     </FixedSizeList>
   </Document>
 );
}
Enter fullscreen mode Exit fullscreen mode

This assumes uniform page sizes. If your documents mix portrait and landscape pages, read each page's dimensions and use a variable-size list instead. Also check the API of the version of react-window you install, as newer major versions changed the component signatures.

4. Control Resolution Deliberately

Canvas cost scales with pixel count. On a 3x display, a page rendered at full device pixel ratio can carry roughly nine times the pixels of a 1x render. Most users will not notice the difference between 2x and 3x for reading a document, but your memory usage will.

<Page
 pageNumber={n}
 width={600}
 devicePixelRatio={Math.min(window.devicePixelRatio || 1, 2)}
/>
Enter fullscreen mode Exit fullscreen mode

Mobile browsers also impose canvas size limits, and exceeding them can produce blank pages or crashes. Capping the ratio is a cheap safeguard. For thumbnail sidebars, go even lower and render at a small width, because a 120px preview does not need a full-resolution canvas.

5. Turn Off Layers You Do Not Need

Each Page can render a text layer (for selection and search) and an annotation layer (for links and form fields). Both add DOM nodes and processing time. If your use case is a read-only preview or a thumbnail strip, switch them off:

<Page pageNumber={n} renderTextLayer={false} renderAnnotationLayer={false} />
Enter fullscreen mode Exit fullscreen mode

Keep the text layer on only where users truly need to select or search text. A good pattern is to disable it for thumbnails and enable it for the main reading pane.

6. Load Large Files Smarter

By default, the browser may download the whole file before you see anything useful. PDF.js supports range requests and streaming, which let it fetch only the bytes it needs for the pages being viewed. Your server must return Accept-Ranges headers and support partial content responses for this to work. You can then tune loading through the options object:

const options = {
 disableAutoFetch: true, // do not prefetch the entire file
 rangeChunkSize: 65536,  // size of each requested chunk
};
Enter fullscreen mode Exit fullscreen mode

Test this against your real hosting setup. Some CDNs and proxies strip range support, and then the optimization silently does nothing.

7. Clean Up After Yourself

react-pdf destroys documents when components unmount, but if you work with pdfjs-dist directly, cleanup is your job. Call pdf.destroy() when you are done with a document and page.cleanup() after rendering pages you will not revisit soon. Also avoid holding references to old canvases in state. Memory leaks in viewers are slow and quiet, and they usually show up only after a user has flipped through several large files in one session.

8. Sometimes the Problem Is the File, Not the Code

Here is a lesson that took many developers too long to learn: you can optimize the viewer endlessly and still struggle with a 90 MB scan where every page is a full-resolution image. No amount of virtualization makes a bloated file light.

This is where it helps to treat the document as part of the pipeline. Before wiring up your viewer, try preparing the file itself. If a giant document is only needed in part, split it into smaller sections. If files are oversized, compress them. Doing this in a browser-based toolkit like PDF Conveter is a handy way to do it, since it is free and runs in the browser, so there is nothing to install just to get a heavier test file down to a realistic size.

It is also useful for the reverse case. Need to stress-test your viewer? Build a deliberately heavy fixture by merging several documents, then compare the render behavior before and after compression. Having a small set of light, medium, and heavy PDFs turns performance work from guesswork into a repeatable benchmark. If you ship a product where users upload PDFs, consider guiding them to reduce file size before upload, which saves bandwidth for everyone.

A Quick Performance Checklist

Before shipping your viewer, run through this list:

  • The worker is configured once, with a matching version.
  • options and file props are stable across renders.
  • Pages are virtualized, not all mounted at once.
  • Device pixel ratio is capped, and thumbnails use small widths.
  • Text and annotation layers are off wherever they are not needed.
  • Range requests are verified against your real server or CDN.
  • Documents and pages are cleaned up on unmount.
  • You have tested with light, medium, and very heavy PDFs, on a mid-range phone as well as your dev machine. Measure before and after each change with the browser's Performance panel and memory profiler. Assumptions about what is slow are wrong surprisingly often.

Final Thoughts

Good PDF performance in React is rarely about one clever trick. It is about respecting how expensive rasterization is, then trimming the work at every layer: fewer pages mounted, fewer pixels drawn, fewer features enabled, and lighter files going in. Apply these habits early, and that 300-page contract becomes just another Tuesday instead of a production incident.

If you are building something with PDFs, start with a lean viewer, build a small set of test files, and only add features once the basics run smoothly.

Top comments (0)