← dompdf.js Studio

Chaining and Batch Rendering: Multi-Page Documents and Bulk Export in Practice

Single-page PDFs are only the beginning; production work is dominated by multi-page and batch requirements. A report needs a cover, a table of contents, body chapters, and an appendix assembled layer by layer; a certificate system must export hundreds of PDFs with identical layouts and different data in a single run. These requirements stress code organization and performance alike: how do you guarantee page assembly order? How do you keep a batch loop from freezing the browser? How long does rendering dozens of pages take, and will users wait? The dompdf.js API is designed for exactly these scenarios: addPage can be chained to assemble multi-page documents, instances can be reused to power batch loops, and the WASM core keeps long-document rendering fast. This guide starts with how to organize chained calls, then covers real batch scenarios — certificates and reports — and digs into performance optimization and progress feedback, plus the partial-success and ordering decisions that separate production batches from prototypes. By the end, batch export will be a stable, fast, pleasant feature rather than a source of anxiety. We also cover the audit trail that makes every run verifiable and the concurrency limits that keep heavy loads stable, so large batches stay predictable at any scale.

Chaining: Assembling Multi-Page Documents

The dompdf.js multi-page model is direct: every addPage call appends a page, and page order equals call order. The value of chaining is that assembling a document becomes writing code: the cover, table of contents, body, and appendix are each one addPage call, so reading the code reveals the document structure. Modifying one page means locating one call — maintainability that giant hand-sliced HTML strings simply cannot offer.

Chaining also accommodates mixed content forms: string-template pages and DOM-element pages can alternate, letting data-driven dynamic pages and static pages coexist. A report might render its cover from a string template, capture live charts from DOM containers in the middle, and append a data appendix at the end — all in one chain, with each page playing its own role.

Organize the chain by splitting render functions along document structure: each function owns the HTML or element for exactly one page, and the main flow calls addPage in sequence. Single-page functions become independently testable and reusable, the assembly order is legible at a glance, and adding a new page type never disturbs the existing structure. Readability and extensibility improve together.

One habit worth adopting: name the render functions after the document sections they produce (renderCover, renderChapter, renderAppendix). The export code then reads like a table of contents, and any mismatch between the code structure and the intended document structure becomes visible immediately — the code and the document tell the same story.

And keep the chain deterministic: no conditional pages that depend on mutable global state at call time. If a page's inclusion varies, compute that decision once and pass it explicitly — deterministic assembly makes the output reproducible, which is the foundation of both testing and debugging.

Chaining pays an extra dividend at test time: every page render function takes data and returns HTML, so it is a pure function that is trivial to assert on, and the main flow only needs to verify call order and count. Tests cover both the template layer and the assembly layer, regression risk drops, and refactoring the document structure stops being scary.

Batch Rendering: Looping Certificates and Reports

The canonical batch shape is: a data array, one unified template, and a loop. Take certificates: award lists of dozens or hundreds of people, each with a different name, prize, and serial number, all sharing one layout. Write the certificate template as a function that takes the data, loop over the list, and save each result — a list that took hours of manual layout is exported in seconds. The productivity jump is the entire point of the feature.

Two decisions shape every batch implementation. First, per-document instances versus a shared instance: when layout and configuration are identical, create a fresh instance inside the loop so exports never interfere with each other; when data volume is large, reuse a configuration factory to cut repeated initialization cost. The combination gives you both isolation and performance.

Second, the output strategy: download each document separately (one file per document) or merge into a single multi-page document. Certificates and contracts that must be archived individually suit per-file downloads; reports and dossiers meant to be read as a whole suit a merged document. Choose by how the business actually delivers the files — never sacrifice delivery convenience for technical uniformity.

A third, often-overlooked decision is ordering: does the batch need a stable, meaningful order (sorted by name, by rank, by date)? Sort the input data before the loop, not the output files after — the template loop preserves input order, so the sort belongs on the data. Stable ordering makes spot-checking and auditing the batch trivial.

And plan for partial success: a batch of two hundred certificates will occasionally hit a bad record. Loop with per-item try/catch, record failures, continue, and report '197 succeeded, 3 failed' at the end. Partial success with a clear summary beats all-or-nothing failure every time — users can fix the three records and re-run just those.

Batch data deserves its own stability layer: paginate API fetches, handle incremental updates, and implement retries outside the render loop. Prepare a complete, ordered array first, then enter the render loop — render logic stays pure, data problems and render problems never entangle, and each is debugged in its own layer.

A final production habit for batch features: keep an audit trail of every run. Record the run start time, the input count, the success and failure counts, and the list of failed records with their reasons, then surface the summary in the UI and log the details. When a user reports that three certificates are missing, the audit trail answers instantly whether they failed to render or were never in the input — a distinction that would otherwise take a long conversation to establish. The audit log also feeds the success-rate metrics described earlier, closing the loop between operations and analytics. Batch features run unattended for years; the audit trail is what makes them auditable, debuggable, and ultimately trustworthy.

Complete Example: Batch Certificate Generation

This example loops over an award list, renders each certificate from a data-driven template, and saves each file with a dynamic name. Note the fresh instance per certificate, which guarantees that one document's state never leaks into another's — the safest pattern for batch isolation.

import { DomPDF } from 'dompdf.js';

const winners = [
  { name: 'Zhang Wei', prize: 'First Prize' },
  { name: 'Li Na', prize: 'Second Prize' },
  { name: 'Wang Qiang', prize: 'Third Prize' },
];

// Certificate template: takes data, returns an HTML string
function certificateHTML(w) {
  return `
    <div style="text-align:center;padding-top:60mm">
      <h1>Certificate of Award</h1>
      <p style="font-size:18pt">${w.name}</p>
      <p>won ${w.prize} in the 2026 Programming Contest.</p>
    </div>`;
}

// Batch export: per-file download with dynamic names
for (const w of winners) {
  const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
  pdf.addPage(certificateHTML(w));
  pdf.save(`certificate-${w.name}.pdf`);
}

Performance Tuning: Making Batch Export Faster

Batch time is dominated by rendering: every document runs the full snapshot, pagination, and write pipeline. Optimization starts with the template, which is where the biggest wins live: keep templates lean, avoid unnecessary nesting and complex styling, and reuse one template string across the batch instead of rebuilding it per record. Render time drops measurably as template complexity falls.

Images are the second optimization front: if every certificate re-loads and re-encodes the same logo and seal, batch cost multiplies. Convert shared images to inline base64 and reference the same URL so the resource is encoded exactly once; keep image dimensions reasonable, because oversized images slow down every single document and inflate every file. One-time prep, batch-wide benefit.

Render granularity matters too: if users want one merged multi-page document, build it with a single instance and one save call instead of repeated initialization and downloads; if they need per-file output, pace the batch loop, run in chunks if necessary, and give progress feedback so users are never staring at an unresponsive page.

A measurement habit pays off: time a single-document render, then extrapolate to the batch size before shipping. If 200 certificates at 300ms each means a minute of waiting, the UX plan changes — chunking, progress, or background generation becomes a requirement, not an afterthought. Know the numbers before users discover them.

And watch the memory profile: long documents and heavy images are the main consumers. Release references after each render (null out variables, remove temporary DOM), compress large images before embedding, and process in batches when needed. Lower peak memory means longer batch runs stay stable instead of degrading progressively.

One template-level win is easy to miss: hoist shared styles into a <style> tag in the template string instead of repeating inline styles on every element. The template shrinks, the snapshot stage has fewer styles to process, batch rendering benefits disproportionately, and maintenance gets easier — three wins from one structural choice.

Progress Feedback and User Experience

A batch export that takes tens of seconds with no feedback makes users wonder whether it is stuck. Maintain a counter in the loop — exported count over total — and show it with the current filename in the UI. Users see progress happening, patience improves noticeably, and the duplicate clicks and support questions that accompany opaque waits largely disappear.

Beyond progress, handle failure gracefully: one failed document in a batch must not abort the rest. Wrap each item in its own try/catch, record the reason, continue, and summarize at the end — 'succeeded X, failed Y' — so users know exactly where things stand and can retry just the failed items. Far more reliable than an all-or-nothing rerun, and the work already done is never wasted.

On interaction, disable the trigger during export so users cannot queue duplicate tasks by clicking repeatedly, and show a clear success message with file-location hints when the batch completes. The quality of a batch feature is largely judged on these details; when technique and interaction are both right, the feature is actually finished rather than merely runnable.

Consider also offering a cancellation path for long batches: a 'stop after current file' button lets users bail out gracefully instead of closing the tab, and the partial results remain usable. Real batch features run in production for years; the escape hatch is what keeps them humane.

Finally, set expectations before the run: a line in the UI like 'this will download 200 files and take about a minute' converts an anxious wait into a scheduled one. Users who know what to expect rate the experience far more generously than users who are surprised by the duration.

Design the progress display with two complementary dimensions: an overall progress bar showing 'file X of N' and a per-item status line showing 'now exporting: certificate-Zhang-Wei.pdf'. Users learn how long is left and what is happening right now — the right information density for a long wait, and a small touch that makes the whole run feel intentional.

Common Batch Rendering Problems

Q: The browser freezes during batch export? A: Usually oversized single renders or oversized images. Split content into multiple addPage calls and compress images; if it still stalls, run the batch in chunks with pauses so the browser can reclaim resources and the task queue never grows unboundedly. The page stays responsive throughout.

Q: Only the first few files download in the loop? A: Browsers throttle consecutive automatic downloads. Space the downloads out, or let users trigger each one by click; for batch export, prefer a serial plus interval strategy, which matches browser security expectations and raises the success rate dramatically.

Q: One bad page fails the whole multi-page document? A: Isolate that page's template and data, and reproduce it alone with a minimal case to confirm the problem. In production, run a basic per-page validation at the addPage stage — non-empty, correct types — to intercept errors early rather than discovering them after the entire document has rendered.

Q: Memory usage climbs during long batch runs? A: Long documents and many images are the main consumers. Release references as soon as each render finishes, compress large images before embedding, and chunk the batch if needed. Peak memory falls noticeably, and multi-hour batch jobs stay stable instead of slowing down progressively.

And one design question worth asking before writing any batch code: does the batch really need to be one run, or would a queue with per-item retry serve the business better? A queue gives users control over pace, retries, and partial results — for many real-world batch features, that control is the feature.

For genuinely heavy batch loads — thousands of documents — consider spreading the rendering across idle time or capping concurrency. Browser resources are finite; rather than letting one run saturate everything, a measured pace keeps the page responsive and the feature reliable over the long haul, which matters more than raw throughput.

⚡ 现场演示(点击生成 PDF)

下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:

Hello from dompdf.js!

这是由 dompdf.js 渲染的示例 PDF 内容。