This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/react-pdf-performance-optimization.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. react-pdf performance: Memoization, virtualization, DPI

Table of contents

    This guide covers optimizing react-pdf performance — the most common pitfalls like unmemoized props and high-DPI rendering, plus techniques like page virtualization and server-side range requests.
    react-pdf performance: Memoization, virtualization, DPI
    TL;DR
    • Memoize the file and options props — both compare with ===, so a fresh object on every render triggers a refetch loop.
    • Cap devicePixelRatio (e.g. Math.min(2, window.devicePixelRatio)) — 3× rendering uses ~9× the memory of 1×.
    • Render only the pages the user can see. A page window of 3 is enough for most viewers; reach for react-window’s VariableSizeList if your PDF has mixed page sizes.
    • Disable unused layers (renderTextLayer={false}, renderAnnotationLayer={false}) and make sure your server returns Accept-Ranges: bytes so PDF.js can stream byte ranges.

    react-pdf can consume significant memory and CPU, especially with large documents or high-dots-per-inch (DPI) displays. This guide covers the officially recommended optimization techniques.

    1. Memoize the file prop

    The most common react-pdf performance issue: The file prop uses strict equality (===). If you create a new object each render, the PDF refetches every time:

    // BAD — new object every render → infinite refetch loop.
    function Viewer({ url }) {
    return <Document file={{ url }} />;
    }
    // GOOD — memoized.
    function Viewer({ url }) {
    const file = useMemo(() => ({ url }), [url]);
    return <Document file={file} />;
    }
    // GOOD — stored in state.
    function Viewer({ url }) {
    const [file] = useState(() => ({ url }));
    return <Document file={file} />;
    }
    // GOOD — string URL (primitives are compared by value).
    function Viewer({ url }) {
    return <Document file={url} />;
    }

    2. Memoize the options prop

    The same issue applies to options:

    // BAD — new object every render.
    <Document options={{ cMapUrl: "/cmaps/" }} />
    // GOOD — defined outside component.
    const options = { cMapUrl: "/cmaps/" };
    function Viewer() {
    return <Document file={file} options={options} />;
    }
    // GOOD — memoized inside component.
    function Viewer() {
    const options = useMemo(() => ({ cMapUrl: "/cmaps/" }), []);
    return <Document file={file} options={options} />;
    }

    3. Cap device pixel ratio

    On high-DPI displays (Retina, 2× or 3×), PDF.js renders the canvas at the same multiplier — that’s 4× or 9× the pixels, and roughly 4× or 9× the memory. Cap it:

    <Page
    pageNumber={1}
    devicePixelRatio={Math.min(2, window.devicePixelRatio)}
    />

    For thumbnails, use an even lower devicePixelRatio cap:

    <Thumbnail
    pageNumber={1}
    width={150}
    devicePixelRatio={1}
    />
    DPICanvas for 800px wide pageMemory
    800 × 1035~3.3 MB
    1600 × 2070~13.2 MB
    2400 × 3105~29.8 MB

    The memory required is width × height × 4 bytes (RGBA, one byte per channel).

    4. Render only visible pages

    Don’t render all pages at once for large documents. Use a windowed approach:

    function VirtualizedPDF() {
    const [numPages, setNumPages] = useState(null);
    const [currentPage, setCurrentPage] = useState(1);
    const windowSize = 3; // Render current page +/- 1.
    const visiblePages = useMemo(() => {
    if (!numPages) return [];
    const start = Math.max(1, currentPage - Math.floor(windowSize / 2));
    const end = Math.min(numPages, start + windowSize - 1);
    return Array.from({ length: end - start + 1 }, (_, i) => start + i);
    }, [currentPage, numPages, windowSize]);
    return (
    <Document
    file={file}
    onLoadSuccess={({ numPages }) => setNumPages(numPages)}
    >
    {visiblePages.map((pageNum) => (
    <Page key={pageNum} pageNumber={pageNum} />
    ))}
    </Document>
    );
    }

    For a full virtual scroll implementation, use a library like react-window or react-virtualized:

    import { FixedSizeList } from "react-window";
    function VirtualScrollPDF({ numPages }) {
    return (
    <Document file={file}>
    <FixedSizeList
    height={800}
    width={600}
    itemCount={numPages}
    itemSize={1035} // Approximate page height.
    >
    {({ index, style }) => (
    <div style={style}>
    <Page pageNumber={index + 1} width={600} />
    </div>
    )}
    </FixedSizeList>
    </Document>
    );
    }

    FixedSizeList assumes every page is the same height. If your PDF has mixed page sizes (portrait/landscape, A4/Letter, foldouts), switch to VariableSizeList and compute each page’s height from the viewport returned by <Page>’s onLoadSuccess callback.

    5. Don’t resize canvas with CSS

    Never use CSS width/height or transforms to resize the canvas. This doesn’t change the rendering resolution — it just stretches the pixels, causing blurriness or wasted memory:

    // BAD — CSS resize (stretches pixels, doesn't change render resolution).
    <Page pageNumber={1} />
    // .react-pdf__Page canvas { width: 400px; } ← in your stylesheet.
    // GOOD — use the `width` prop.
    <Page pageNumber={1} width={400} />

    6. Disable unnecessary layers

    If you don’t need text selection or annotations, disable them:

    <Page
    pageNumber={1}
    renderTextLayer={false} // Skip text layer.
    renderAnnotationLayer={false} // Skip annotation layer.
    />

    Each layer adds document object model (DOM) elements and processing time.

    7. Use renderMode “none” for data-only

    This is useful when you only need text or annotation data, not visual rendering:

    <Page
    pageNumber={1}
    renderMode="none"
    renderTextLayer={false}
    renderAnnotationLayer={false}
    onGetTextSuccess={({ items }) => {
    // Process text without rendering.
    }}
    />

    8. Server-side considerations

    Ensure your server supports HTTP 206 (Partial Content) for range requests. Range request support allows PDF.js to download only the needed parts of the PDF instead of the entire file:

    Accept-Ranges: bytes
    Content-Range: bytes 0-65535/1234567

    Without range request support, the entire PDF must download before the first page renders.

    Summary checklist

    OptimizationImpactEffort
    Memoize file propCriticalLow
    Memoize options propHighLow
    Cap devicePixelRatioHighLow
    Virtualize pagesHighMedium
    Disable unused layersMediumLow
    Use width prop not CSSMediumLow
    Enable range requests (server)HighMedium
    Lower thumbnail DPIMediumLow

    How Nutrient Web SDK handles this

    Every optimization technique in this guide is handled automatically by Nutrient Web SDK’s WebAssembly (WASM) rendering engine:

    // No memoization, no DPI capping, no virtualization, no layer toggling.
    // All optimizations are automatic.
    const instance = await NutrientViewer.load({
    container: "#pdf-container",
    document: "document.pdf",
    // WASM rendering engine handles:
    // - Progressive loading and render prioritization.
    // - Automatic memory management.
    // - DPI-aware rendering without canvas bloat.
    // - Only visible pages rendered.
    });

    Most of the client-side optimizations in this guide — memoizing props, capping DPI, virtualizing pages, disabling layers, managing memory — are handled automatically by Nutrient’s WASM rendering engine. Range requests still depend on your server returning Accept-Ranges: bytes, but Nutrient takes advantage of them automatically when available.

    Learn more about Nutrient Web SDK | Migration guide | Contact Sales

    FAQ

    Why is my PDF refetching on every render?

    The file prop is compared by reference (===). If you pass an object literal like <Document file={{ url }} />, React sees a new object on every render and react-pdf reloads the document. Memoize the object with useMemo, lift it to state, define it outside the component, or pass a plain string URL.

    What’s a safe devicePixelRatio cap?

    Math.min(2, window.devicePixelRatio) is the common compromise — sharp on Retina, half the memory of a 3× render on phones. For thumbnails, set devicePixelRatio={1}. If you’re targeting strictly print-quality output, leave it at the native value and accept the memory cost.

    How big a page window should I render?

    For most viewers, the current page plus one above and one below is enough. Start at three and increase only if users see flashes of blank space when scrolling fast. Larger windows trade memory for fewer mid-scroll renders.

    Can I use react-window if my PDF has different page sizes?

    Yes — use VariableSizeList instead of FixedSizeList. Compute each page’s height from the viewport you get back in <Page>’s onLoadSuccess (or preload page metadata via pdf.getPage()). Then feed those heights into the list.

    Does disabling the text layer break copy-paste?

    Yes. The text layer is what makes the rendered page text-selectable and searchable in the DOM. Only disable it for view-only contexts (thumbnails, previews, screenshots-as-images). If you need search but not selection, keep the text layer and hide it visually instead.

    How do I tell whether my server supports range requests?

    Send a HEAD request and look for Accept-Ranges: bytes in the response. Or open the DevTools Network panel, load the PDF, and check for multiple 206 Partial Content responses with Range: request headers — that’s PDF.js streaming byte ranges. Some content delivery network (CDN) configurations strip Accept-Ranges for gzipped content; serve PDFs uncompressed (Content-Encoding: identity).

    Austin Nguyen

    Austin Nguyen

    AI Engineer

    When Austin isn’t pulling all-nighters to build new features, he enjoys watching science videos on YouTube and cooking.

    Explore related topics

    Try for free Ready to get started?