How far does it go?
Generate up to 100,000 pages in this tab. No server, no upload, no install. The point is not that it is fast — it is that the document is never fully assembled as one binary, so the ceiling is your machine's memory rather than a hard limit in the library.
Progress
Why phase 2 has no percentage. pdfnative builds every PDF object before emitting the first byte, and that loop yields nothing while it runs. There is no progress signal to read, so drawing a bar there would mean inventing one. Phase 3 shows a byte counter rather than a percentage for the same reason — the total size is not known until the generator finishes. See the streaming guide for the exact memory profile.
Result
What this demonstrates
- No joined binary.
buildDocumentPDFStreamTrueassembles the PDF as an array of parts and frees each one as it is emitted, so the fully-joined document never exists. Its siblingbuildDocumentPDFStreamjoins everything into a single JavaScript string first, and V8 caps a string at roughly 512 MB — that variant cannot reach this scale at any content density. - Off the main thread. Generation runs in an ES-module Web Worker, so the page stays interactive and Cancel actually works. Cancelling terminates the worker; there is no cooperative abort point, because assembly is a synchronous loop.
- Nothing is uploaded. Every byte is produced in this tab. The measure-only sink never accumulates the output at all — it counts bytes as they arrive and discards them.
- The page count is measured, not assumed. With the checkbox on, the worker scans the emitted bytes for
/Type /Pageobject headers and reports what it actually found. Turn it off and the tile is labelled (requested) instead.
The content is deliberately lean
Each page carries a heading and a paragraph — between 496 and 509 bytes of output per page depending on document length (509 at 100,000 pages). That is a deliberate choice, not a flattering one: it makes the page count exact and known before generation starts, which is what lets the pages-per-second figure mean something instead of being derived from a byte estimate. Denser pages (tables, embedded fonts, images) produce proportionally more bytes and take proportionally longer.
If you want to see a realistic document instead of a scalability floor, the PDF Toolkit and Charts playgrounds build ordinary documents with real content.
Code
import { buildDocumentPDFStreamTrue } from 'pdfnative';
// Exactly one page per iteration: a page break separates consecutive pages.
function leanBlocks(pages) {
const blocks = [];
for (let i = 0; i < pages; i++) {
if (i > 0) blocks.push({ type: 'pageBreak' });
blocks.push({ type: 'heading', text: `Section ${i + 1}`, level: 2 });
blocks.push({ type: 'paragraph', text: `Page ${i + 1} of ${pages}. …` });
}
return blocks;
}
const blocks = leanBlocks(100_000);
let bytes = 0;
// Supply the footer template explicitly: relying on `footerText` also pulls in
// the default template, which stamps `{page}/{pages}` on the right.
for await (const chunk of buildDocumentPDFStreamTrue(
{ title: 'Scale', blocks, layout: { footerTemplate: { center: 'page {page}' } } },
{ maxBlocks: blocks.length + 1024 }, // the default rail is 100,000
{ chunkSize: 262_144 },
)) {
bytes += chunk.length; // …or write it straight to disk
}
Two constraints are validated at the boundary and throw: streaming rejects toc blocks, and rejects the {pages} placeholder in a header or footer template you supply. Worth being precise about the second one: buildDocumentPDFStreamTrue finishes assembling before it yields, so the total page count is in fact known — the rejection is a conservative rule that will matter once page-at-a-time assembly lands. Note also that it only inspects a template you pass; the built-in default does use {pages} and resolves it.