feat: new pagination engine and Page layout prop - #3502
Merged
Conversation
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep fixed nodes nested inside flow containers in the item stream instead of stripping them at every depth, and mark their items repeat: true so the paginate engine re-emits a fresh copy on every continuation of the enclosing container. The top-level filter (direct page children) is unchanged: those still go through the flowTop band in index.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sketch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atePaginator ships independently Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eights paginate(root, height) becomes the constant-height loop over the sealed iterator, keeping its MAX_PAGES backstop. Iterator callers pass a height per next() call, own their termination, and cannot mutate in-flight state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Names what comes back — fresh Fragments of the completed repeat items — instead of where the caller happens to put them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The slot renders empty and the page's instance children graft in directly (they already passed createInstances), so content measures at slot width inside the chrome on the unconstrained first pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Template pages route through createPaginator: chrome is instantiated and measured per page (render props resolved with that page's number), the slot's measured box is the flow region, and each page is built by filling the measured chrome's slot with that page's fragment. Width variance and zero-height slots fail loudly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Template pages always take the totals round (a layout may read totalPages from params without any render prop), width variance fails naming the page, and mirrored asides pass since the invariant binds width only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pages without a layout prop get one synthesized from their own children: in-flow fixed stays as chrome around the slot, flow content goes inside, absolutes ride to page 1. splitPage becomes the one per-page loop, and the flowTop band arithmetic is gone — the slot's measured box is the flow region for every page. The slot always stretches (alignment belongs to its children), inherits the page's flow styles, and stays transparent to percent-height resolution and test walkers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…opped The unified template model reserves its space by construction, so the prefix-only policy and its staged deprecation are unnecessary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 23, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Closes: #3499
Replaces the pagination step with the new
@react-pdf/paginateengine and adds alayoutprop toPagefor per-page chrome (headers, footers, sidebars — anything that repeats around the content).<Page layout={Layout}>: the layout component receives{ pageNumber, totalPages, ... }and the page content aschildren, and runs once per output page.fixedsemantic: in-flow fixed elements repeat at the top of every page they span. Footers are the layout's job.Performance
The old engine relayouts on every split, so cost grows quadratically with document size. The new engine is linear. Same documents, same output page counts:
With a
layouton every page: ~1 ms per page, flat at every size.Breaking
Ships as a major.
fixedsuffix elements no longer reserve footer space (uselayout), flex justify at the page root follows the new stream semantics, and render props handed to@react-pdf/layoutdirectly must return instances (the renderer does this for you).minPresenceAheadis not ported yet — pending decision.Testing
🤖 Generated with Claude Code
Supersedes
Open PRs made obsolete by this engine (closed manually, since the merge predates this note):
O(N²)→O(N): this rewrite is the full redesign that thread pointed to; the new engine benches 2000 elements → 200 pages in ~41 ms. Also resolves Performance problem with creating multiple pages #2448 and Slow generation time for PDF with many pages #3181.wrap={false}child squashed/overlapping at a page break: the engine defers it to the next page instead of force-fitting.Pagelayoutprop (pageNumber/subPageNumberper output page).createPaginatorAPI is the natural hook for this now.Issues resolved
Each of these was verified by running its repro against the new engine (closed manually):
Squashed / overlapping content at page breaks — the engine defers or splits instead of force-fitting:
wrap={false}content compressed or drawn over itself near a page boundarymarginBottomnear the page bottom hung pagination foreverfixedelement made content shrink and overlap on wrapping pagesmarginBottomnear the page bottom froze the browser tab (same root cause as Margin appearing near the bottom of page causes react pdf to hang #2996)Dynamic nodes — pagination re-runs with real page numbers instead of freezing a partition and relayouting:
renderprop breaks the pagination #3173, Content in pages with dynamic components can get squashed #2476, Text overlaps or squashes at page breaks in dynamic content #3298, Page break depending on the height of dynamic components #2378 — render-prop content had its height ignored or got squashedtotalPagesnow reaches every render prop, includingViewsfixedflex-row header didn't show<Text>element on the last page (because<View>is not usable in this case) #1005 — hiding an element on the last page now works from any render propminPresenceAhead — reimplemented as a forbid window (#3504):
minPresenceAheadis not working as expected #2658, minPresenceAhead Not working? #3233, minPresenceAhead - calculating the meaning of 'presence' on next sibling element #955 — the prop was ignored or its semantics unclear; a header near the page bottom now moves to the next page with its contentOther:
wrap={false}onPagenow yields a single pagelineHeighton aTextinside a flex row hung the process foreverflexDirection: rowview wrapping across pages produced broken geometry / overlapping cells on the next pageposition: absoluteblock (withoutfixed) repeated on every pagesetNodeHeightcrash on heavy documents (the crashing split path no longer exists)Enabled by the
layoutprop (the supported pattern for what these asked for):totalPagesin layout params)subPageNumberin layout params)