Reports, contracts, manuals, and product documentation — documents of dozens or hundreds of pages — concentrate all the performance pressure of PDF generation in one place. More pages, more images, and more complex styles stack on top of each other, and generation time can climb from seconds to minutes while users wait and server resources strain. dompdf.js runs its Rust and WebAssembly core entirely in the browser, handling snapshot, layout, rendering, and packaging in one pipeline, so its performance ceiling is high; but the results you actually get depend heavily on how you call it. Template complexity, image handling, pagination strategy, and instance reuse each move the final timing in measurable ways. This guide starts with a bottleneck analysis of large documents, then covers the trade-offs in pagination strategy and the scale parameter, the standard batch-generation code pattern, image compression and downsampling, stylesheet slimming and DOM reduction, and closes with benchmark methodology and an ordered optimization checklist. The goal is concrete: to bring a hundred-page document's generation time into an acceptable range, with data to prove every change you make, without sacrificing output quality along the way. The checklist is ordered by return on effort, so even a quick pass through the first two items produces measurable gains, and the benchmark method gives you the evidence to defend every change to stakeholders. Whether you are tuning a ten-page invoice or a two-hundred-page manual, the same measurement-driven approach applies. The techniques are complementary: none of them requires exotic tooling, and each compounds with the others, which is why a systematic pass beats isolated tweaks.
The cost of a large document concentrates in three stages: DOM snapshotting and style computation, image decoding and resampling, and the WASM-side layout and rendering. More pages raise the cumulative cost of layout and rendering; larger and more numerous images raise decoding time and memory; more complex styles raise the per-page snapshot cost. The three factors compound, which is why a large document is not simply a scaled-up small one — the bottlenecks interact and amplify each other.
Identifying the bottleneck must be measurement, not guessing. Use performance.now() to record the time spent in snapshot, generation, and save separately, and compare the timing curves across different page counts and image counts. The stage whose cost grows fastest with content is your real target; optimizing elsewhere is wasted effort. This measurement-first habit is the foundation of every other technique in this guide, and it pays off on the first document you profile.
There is also a factor that is easy to overlook: main-thread blocking. If the page freezes during generation, the perceived wait is far longer than the actual elapsed time. dompdf.js runs the WASM pipeline in a Worker, so the main thread stays responsive, but snapshotting and resource preparation still happen there. Slimming those two phases improves the felt experience as much as raw throughput, and users judge quality by responsiveness as much as by the final timing.
One more factor deserves attention: the generation environment itself. Browser extensions, hardware acceleration settings, and even the tab's background state can shift timings by surprising margins, so when comparing measurements, keep the environment fixed and note the browser version alongside the numbers. That discipline makes your benchmark a reliable instrument instead of a noisy one.
dompdf.js supports automatic pagination: content that exceeds one page is split onto the next, and continuous content is handled with a single addPage call. Automatic pagination suits flowing documents and long-form text, where the engine's break decisions are usually fine. But blocks that must not be cut in half — tables, cards, code listings — should declare break-inside: avoid so each block lands complete on a single page, which produces a noticeably more professional layout.
For documents with strict layout requirements, manual pagination is the answer: split the content into page-sized pieces by business logic and add each one with addPage, giving you complete control over where every page starts and ends. The cost is that you own the content division; the payoff is stable, predictable pages and exact page counts. Contracts, tickets, and certificates are the canonical cases where manual control is worth the extra work.
A hybrid strategy is the most practical: flowing body text uses automatic pagination, special blocks are constrained with break-inside, and key pages are split manually. Whichever strategy you choose, verify the pagination against real content — confirm nothing is truncated and no block straddles a page break awkwardly. Correct pagination is the quality floor of any document and outranks performance; optimize speed only after the layout is right.
When content is generated dynamically, pagination verification should be automated: render a representative set of documents in CI and assert page counts and block boundaries. Manual eyeballing catches gross errors but misses the occasional overflow that only appears with certain data lengths, and automated checks turn pagination from a recurring risk into a regression test.
A practical note on verification: automated pagination checks belong in CI, where a representative document set is rendered on every merge and the page count and block boundaries are asserted. That turns pagination from a recurring manual inspection into a regression test, and it catches the content-length edge cases that only appear with real data.
The standard pattern for multi-page generation is a loop of addPage calls: one DomPDF instance accepts several page contents in sequence and a single save at the end produces the file. Reusing the instance avoids repeated initialization overhead and shares one rendering context across pages; because addPage returns a Promise, awaiting inside the loop sequences the pages deterministically instead of letting them compete for resources.
Sequential processing also makes progress feedback natural: update a progress indicator after every completed page, and the user sees the generation advancing. When all page contents are known in advance, prepare the HTML for every page first and then render them in one pass; when pages are produced dynamically, generate and submit each page as it becomes ready. Both orders are clean; choose based on how much of the content exists before rendering starts.
The example below generates a ten-page document with a loop: each page is a template string, the loop awaits pdf.addPage, and a single save finishes the job. The template uses the CSS @page margin boxes with counter(page) and counter(pages) to render page numbers in the footer, so numbering increments automatically across the document with no manual bookkeeping. The same pattern scales to hundreds of pages by changing the loop bounds and the content source.
For very long documents, consider rendering in chunks and saving intermediate progress: if the browser tab is closed mid-generation, a chunked flow can resume from the last completed page instead of losing everything. The chunk boundary is a natural place to checkpoint, and the added complexity is small compared with the cost of re-rendering a two-hundred-page document from scratch.
The pattern also composes with the rest of the performance toolkit: each page's HTML can be built by the same template functions used elsewhere, images can be preprocessed before the loop starts, and the loop itself is the natural place to insert progress reporting. The result is a single, readable flow that scales from ten pages to a thousand by changing data, not structure.
import { DomPDF } from 'dompdf.js';
const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
for (let i = 1; i <= 10; i++) {
const html = `
<style>
@page { @bottom-center { content: counter(page) ' / ' counter(pages); } }
body { font-family: 'Source Han Sans SC', sans-serif; }
</style>
<h2>Chapter ${i}</h2>
<p>This is the content of page ${i}, generated in a loop.</p>`;
await pdf.addPage(html, { format: 'A4' });
console.log('page', i, 'done');
}
pdf.save('report-10pages.pdf');
The image strategy decides both size and memory for large documents: a single 4000×3000 photograph occupies roughly 48MB of memory once decoded, and ten of them approach half a gigabyte, which strains any pipeline. The governing principle is downsample before embed: any image more than twice its display size should be scaled down to roughly display resolution with a canvas, with no visible loss and a dramatic reduction in both memory and file size.
Format choice matters just as much. Photographs should be converted to JPEG with controlled quality, interface screenshots kept as PNG for sharpness, and transparent assets remain PNG. For batch workflows, write one preprocessing function that decodes, scales, compresses, and encodes uniformly, so every image entering the template has already been optimized and the generation pipeline only ever sees lean assets. The function is the single highest-leverage piece of code in a large-document project.
Avoid loading the same image repeatedly: when one image appears on several pages, load it once and reuse it; referencing the same address lets the browser and the render pipeline hit the cache. Image caching strategy — stable URLs, sensible Cache-Control — pays off handsomely in high-frequency generation scenarios, so plan it at deployment time rather than retrofitting it after the first slow batch.
The trade-off is worth stating plainly: downsampling discards information, so keep the original files in storage and only downsample for the export pipeline. That way a future need for higher-resolution output does not require re-acquiring the source assets, and the PDF generator always consumes the leanest version appropriate to its purpose. If a document genuinely needs print-quality images, bypass the pipeline for those specific assets rather than weakening the default.
Style complexity directly raises the per-page layout cost: deeply nested flex and grid layouts, many shadows and filters, and oversized stylesheets all slow rendering. Large documents favor flat structure: reduce unnecessary nesting, prefer simple block layout over elaborate flex arrangements where the design allows, and declare only the rules the template actually uses instead of importing an entire design-system stylesheet.
DOM reduction is equally effective: keep only the content that will actually appear, and strip hidden elements, debug nodes, and repeated structures before generation; render long lists page by page instead of materializing the whole dataset at once. A smaller DOM snapshots faster and lays out faster on the WASM side, and this is the highest-ROI optimization available — it costs nothing, applies to every document, and compounds with every other technique.
Dynamic data should not cause style churn: pin down fonts, image dimensions, and line heights before generation so layout does not shift mid-render and force repeated layout passes. Treat the template as a stable rendering input — data fills it, styles present it — and the output becomes reproducible and the timing predictable. A stable template is also what makes benchmarking meaningful, because measurements stop bouncing between runs.
Watch out for style rules that trigger full re-layouts per page, such as viewport-relative units or rules that depend on the containing block size. In a paginated flow these are evaluated per page and can multiply the layout cost; replacing them with page-relative sizing keeps the per-page cost flat as the document grows.
A useful heuristic: if a template takes more than a few seconds to snapshot, the DOM is probably doing more work than the output needs. Strip decorative wrappers, collapse redundant containers, and remove elements that render nothing — each removal reduces the snapshot and layout cost proportionally, and the visual output stays identical.
Establish a baseline before optimizing anything: fix a representative test document (say one hundred pages with fifty images), and record generation time, PDF size, and memory peak as the reference for every subsequent change. Run the baseline on the same browser and machine with controlled variables so the data is comparable, and re-run it after each optimization to confirm the gain with numbers rather than impressions. A baseline you trust is the difference between directed tuning and superstition.
The optimization checklist, ordered by return: start with DOM reduction and stylesheet slimming (zero cost, large gain), then image downsampling and format unification (low cost, clear gain), and finally adjust scale and pagination strategy (these affect sharpness and layout, so weigh them deliberately). After every change, inspect the output quality to make sure the performance gain did not trade away content fidelity — a fast document with broken layout is not an improvement.
The scale parameter is the leverage point between sharpness and speed: doubling scale quadruples the rendered pixel work, and large documents feel it in both time and memory. Keep the default between 1 and 1.5, raise it only for output that genuinely needs high resolution, or expose an export option so users choose the trade-off themselves. Let the users who need the extra quality pay for it instead of taxing every export with it.
Log the benchmark results with each release: a table of page count, image count, timing, and memory per version turns performance regression into something CI can catch. When a dependency or template change slows the pipeline, the numbers will show it the week it happens instead of months later through user complaints.
Remember that the goal is a stable, predictable export experience, not the absolute minimum milliseconds: a document that generates in eight seconds reliably is better than one that alternates between four and twenty depending on ambient conditions. Consistency is what users actually feel, and the benchmark exists to preserve it across releases.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。