Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

A react-pdf/renderer tab usually becomes unresponsive because PDF layout and generation run synchronously on the browser’s main thread. Move browser generation into a Web Worker, avoid repeated renders with stable inputs, render fewer viewer pages at once, cap excessive pixel density, and use server-side generation when documents are too large or sensitive for the client. The “30 pages” guidance is a warning point, not a guaranteed limit: a complex three-page document can also block the UI.

Why react-pdf/renderer freezes the tab

Generating a PDF is computation-heavy work. Style resolution, text shaping, line breaking, image processing and page breaking execute on the thread that called the renderer. While that synchronous work is running, the browser cannot reliably paint, scroll, handle clicks or process input. Chrome may eventually display its “Page Unresponsive” dialog and offer to abort the script.

React-PDF’s advanced guidance warns that rendering documents with 30 pages or more directly in a browser can occupy the main thread for a long time. Treat that number as a practical warning point rather than a hard cutoff. A short document containing large tables, custom fonts, high-resolution images or difficult wrapping can take longer than a much longer, simple document.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

First identify which operation is stuck

The remedy depends on whether your application is creating a new PDF or displaying an existing one. Use the browser performance panel and your own code path to distinguish these cases.

Generation in the browser

Typical symptoms include a freeze after calling pdf(document).toBlob(), clicking a PDFDownloadLink, or updating a document managed by usePDF. The tab becomes unresponsive before a Blob or download is produced. This is a main-thread generation problem.

Viewing an existing PDF

If the PDF already exists and the freeze occurs while using Document and Page from the React PDF viewer, the issue is simultaneous page rendering, canvas size or download behavior. Viewer fixes do not make local PDF generation asynchronous.

A quick triage checklist

  1. Record the page count and note large tables, long paragraphs, custom fonts, SVGs and raster images.
  2. Check whether the stall starts during toBlob()/PDFDownloadLink or while mounting viewer pages.
  3. Look for new object literals such as file={{url}} or options={{...}} inside a component render.
  4. Test the same document with images, fonts and optional sections removed to find the expensive feature.
  5. Check the installed @react-pdf/renderer, react-pdf, React and bundler versions before applying an old workaround.

Stop accidental repeated generation

Even a document that would finish once can appear frozen if React keeps giving the renderer a new input. React-PDF compares the document and options you provide; fresh objects created on every render can trigger repeated work.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep file and option objects stable

Memoize values whose contents have not changed, and include only their real dependencies:

import { useMemo } from 'react';
import { Document, Page } from 'react-pdf';

function InvoicePreview({ invoiceUrl, password }) {
  const file = useMemo(() => ({ url: invoiceUrl }), [invoiceUrl]);
  const options = useMemo(
    () => ({
      password,
      cMapUrl: '/cmaps/',
      cMapPacked: true
    }),
    [password]
  );

  return (
    <Document file={file} options={options}>
      <Page pageNumber={1} />
    </Document>
  );
}

Do not put a mutable object in state merely to silence a warning; make its ownership and dependencies explicit. If the URL changes, a new value is expected. If nothing changed, the reference should remain the same.

Control expensive updates with usePDF

usePDF is useful when edits to unrelated application state should not immediately recompute the whole document. Keep the document data in stable state or memoized values, update it deliberately, and expose a loading or disabled state while the new Blob is being produced. This is a scheduling and recomputation fix; it does not move work off the main thread.

Be careful with Suspense retries

With current Suspense behavior, keep worker instances, binary inputs and range-transport values outside a subtree that suspends. An initial retry can otherwise recreate those inputs and repeat expensive work. The exact placement depends on your component tree, but the principle is the same: create long-lived inputs at a stable parent rather than inside the suspending component.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Move browser PDF generation into a Web Worker

A Web Worker is the principal fix when the PDF must be created in the browser. The worker has its own JavaScript thread, so layout work no longer blocks the page’s input and painting loop. The document component must be constructed inside the worker: React elements and functions cannot be transferred through postMessage. Send plain structured-cloneable data such as strings, numbers, arrays and asset URLs.

Worker implementation

The following example uses a module worker. It builds the React-PDF document in the worker, returns transferable PDF bytes, and leaves the UI thread responsible for progress and download handling.

// pdf.worker.js
import React from 'react';
import {
  pdf,
  Document,
  Page,
  Text,
  View,
  StyleSheet,
  Font
} from '@react-pdf/renderer';

const styles = StyleSheet.create({
  page: { padding: 36, fontSize: 10 },
  row: { flexDirection: 'row', marginBottom: 6 },
  label: { width: 180 },
  value: { flex: 1 }
});

// Register custom fonts in this worker context, not only in the window.
// Font.register({ family: 'Inter', src: '/fonts/Inter-Regular.ttf' });

function InvoiceDocument({ customer, rows }) {
  return React.createElement(
    Document,
    null,
    React.createElement(
      Page,
      { size: 'A4', style: styles.page },
      React.createElement(Text, null, `Invoice for ${customer}`),
      rows.map((row, index) =>
        React.createElement(
          View,
          { style: styles.row, key: index },
          React.createElement(Text, { style: styles.label }, row.label),
          React.createElement(Text, { style: styles.value }, String(row.value))
        )
      )
    )
  );
}

self.onmessage = async ({ data }) => {
  try {
    const document = React.createElement(InvoiceDocument, data);
    const blob = await pdf(document).toBlob();
    const buffer = await blob.arrayBuffer();
    self.postMessage({ ok: true, buffer }, [buffer]);
  } catch (error) {
    self.postMessage({
      ok: false,
      error: error instanceof Error ? error.message : String(error)
    });
  }
};

UI-side worker wrapper

import { useEffect, useRef, useState } from 'react';

export function usePdfWorker() {
  const workerRef = useRef(null);
  const [state, setState] = useState({ status: 'idle', error: null });

  useEffect(() => {
    const worker = new Worker(
      new URL('./pdf.worker.js', import.meta.url),
      { type: 'module' }
    );
    workerRef.current = worker;
    return () => {
      worker.terminate();
      workerRef.current = null;
    };
  }, []);

  function generate(data) {
    return new Promise((resolve, reject) => {
      const worker = workerRef.current;
      if (!worker) return reject(new Error('PDF worker is not ready'));
      setState({ status: 'running', error: null });

      const onMessage = (event) => {
        worker.removeEventListener('message', onMessage);
        if (!event.data.ok) {
          setState({ status: 'error', error: event.data.error });
          reject(new Error(event.data.error));
          return;
        }
        const blob = new Blob([event.data.buffer], { type: 'application/pdf' });
        setState({ status: 'done', error: null });
        resolve(blob);
      };

      worker.addEventListener('message', onMessage);
      worker.postMessage(data);
    });
  }

  return { generate, state };
}

// Example use:
// const { generate, state } = usePdfWorker();
// const blob = await generate({ customer, rows });
// const href = URL.createObjectURL(blob);
// window.open(href, '_blank');

Bundlers differ in worker entry-point syntax. Vite, webpack and similar tools support new URL('./pdf.worker.js', import.meta.url) with a module worker, but verify the syntax for your build. A worker cannot access the DOM, window-only APIs or components that assume browser globals. Images and font URLs must be reachable from the worker context, and custom fonts must be registered there.

Show progress and handle cancellation

PDF layout is not automatically incremental. Display a “Preparing PDF” state while the worker runs and disable duplicate submissions. If a user navigates away or starts a new job, terminate the old worker and reject its pending promise. For very large documents, split the job into separate PDFs or send it to a server rather than trying to fake progress percentages that the renderer does not provide.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix freezes when displaying an existing PDF

Virtualize long viewers

Rendering many pages at once is compute intensive, even on a fast computer. Render only pages near the viewport and mount or unmount pages as the user scrolls. A virtualization library can manage page positions; the important part is that off-screen pages do not all create canvases simultaneously. Keep the page count and measured heights in a stable model so scrolling does not remount the entire document.

Reduce canvas pixel density when necessary

High-DPI devices multiply the physical pixels in every canvas. Capping the effective device pixel ratio lowers memory and paint cost, at the expense of some sharpness. Apply this only to the viewer path and test text readability on the displays your users actually have. It reduces raster work; it does not speed up the PDF layout algorithm.

Use HTTP range delivery for remote PDFs

When a server hosts an existing PDF, verify that it supports HTTP Partial Content and range requests. A suitable file and server configuration allow the viewer to download only the portions needed for initial pages, improving first-page latency and bandwidth usage. Range delivery cannot make pdf(...).toBlob() asynchronous because that operation is generating a new file locally.

Choose where generation should run

Option Best fit Trade-offs
Web Worker Large documents that must be generated in the browser Requires worker and bundler setup; the worker has no DOM and accepts only serializable data.
Server-side generation Very large, sensitive or consistency-critical documents Adds a backend job and network latency, but removes CPU and memory pressure from the user’s browser.
Viewer virtualization Many pages of an existing PDF are being displayed Reduces simultaneous canvases; it does not accelerate generation of a new PDF.
Controlled usePDF updates Frequent React updates are recomputing an otherwise unchanged document Requires explicit update and state management.
Pixel-density cap High-DPI canvas memory or paint cost is dominant May reduce visual sharpness on some screens.

Make the decision using five questions: where computation can run, how complex the document is, how much latency is acceptable, how difficult the implementation may be, and whether the data can leave the browser. A worker is not a substitute for server generation when the document contains confidential data that policy forbids sending to a client, and a server is unnecessary overhead for a small, simple file that users need immediately offline.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Measure layout complexity instead of relying on page count

Track more than the number of pages. Useful dimensions include the number of table rows, paragraph length, nested views, font families and weights, image count and image dimensions, SVG complexity, wrapping rules and conditional sections. Remove one category at a time in a diagnostic build. If a document becomes responsive when images are removed, resize or compress those assets; if fonts are responsible, confirm that only required families and weights are registered.

The published “30 pages or more” warning comes from browser behavior, not a universal React-PDF limit. Reports of 30-plus-page freezes are user reports rather than controlled cross-device benchmarks, and historical issues show that even a three-page complex layout can stall. Test on the slowest supported device and browser, not only on a developer workstation.

Check versions and build configuration

The React-PDF v4 compatibility documentation lists React 16.8 through React 19 support and notes an esbuild ESM caveat. Confirm that your installed @react-pdf/renderer and viewer package match the React version and bundler mode you actually deploy. Inspect the generated worker chunk in production; a worker that accidentally falls back to the main bundle will not solve the freeze.

A maintainer reported on August 23, 2026 that a browser-freeze problem tracked in issue #2834 was fixed by pull request #3502. If you are applying an old workaround, retest after upgrading to a release containing that fix instead of assuming the workaround remains necessary. Record the exact package versions and build configuration when comparing results.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting common failures

Symptom Likely cause Action
The tab freezes immediately after clicking download. Synchronous toBlob() or PDFDownloadLink work on the main thread. Move document construction and rendering into a Web Worker, or generate on the server.
The worker runs, but custom fonts are missing or the job fails. Fonts were registered only in the window, or their URLs are not available to the worker. Register each font in the worker and verify URL, CORS and bundler asset handling.
Generation starts again after an unrelated state update. New file, options or document objects are created during every render. Use useMemo, state or a stable parent; check Suspense retry boundaries.
Only the viewer freezes while scrolling. Too many pages or high-density canvases are mounted. Virtualize pages and lower effective pixel density where acceptable.
The first page of a remote PDF is slow, but local generation is fine. Server does not provide usable range responses, or the file layout prevents efficient access. Inspect HTTP Partial Content responses and server delivery separately from renderer performance.
Worker creation fails in production. Incorrect worker entry syntax, an unsupported ESM configuration or a missing worker chunk. Follow your bundler’s module-worker syntax, inspect the network panel and verify the deployed chunk.
A previous request finishes after a newer request. Two worker jobs are active and responses are not associated with a request ID. Assign an ID to each job, ignore stale responses, and terminate superseded workers when appropriate.
The browser still reports an unresponsive script after an upgrade. The document remains too complex for the chosen architecture, or the fix is not in the deployed version. Confirm the installed version, profile the reduced document, then choose a worker or server path based on the measured bottleneck.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Validate the fix before shipping

  • Test a small document, a borderline document around the 30-page warning point and your largest realistic file.
  • Test with the largest tables, longest text, all custom fonts and production-sized images enabled.
  • Keep the page interactive during generation: scrolling, typing and cancellation should still work.
  • Check memory usage while opening and closing long viewer documents.
  • Verify that worker errors, font-load failures, network failures and stale responses produce a visible recovery path.
  • Retest on supported browsers and the slowest supported hardware after every renderer or bundler upgrade.

Or skip the browser setup

If your real task is taking a clean screenshot of the web page that contains the PDF workflow, ScreenshotNeo can do that with one request instead of maintaining browser automation. It accepts the cookie or consent banner like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets before capture. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.

See the ScreenshotNeo API documentation for all options. A cURL request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same call in Python:

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)

And in Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. Features include full-page and element capture, device presets and custom viewports, dark mode, retina scale, PDF paper and margin controls, custom CSS and JavaScript, clicks, selector waits, network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification. Parameter names used by other screenshot APIs are accepted to ease migration.

Plan Included shots Price
Free 1,000 per month $0, no card
Starter 3,000 $5
Growth 15,000 $15
Pro 60,000 $39
Scale 250,000 $99
Business 1,000,000 $249

Every feature is included on every plan, and yearly billing gives two months free. You can start with 1,000 free screenshots a month with no card; paid plans start at $5 for 3,000 shots.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Does a 30-page document always need a worker?

No. Thirty pages is a documented warning point, not a guaranteed failure threshold. Measure your actual layout, assets and target hardware; a complex shorter file can still require a worker or server generation.

Can a worker reuse React components from my page?

Only if the worker bundle contains code that is safe without the DOM. Pass plain data and construct the React-PDF document inside the worker; do not try to structured-clone an element or function from the window.

When is server generation preferable to a worker?

Choose the server when documents are very large, contain data that should not be processed in the browser, or must render consistently across devices. A worker is appropriate when generation must remain client-side and the data can safely stay there.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.