-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Environment
- Package:
@react-pdf/renderer - Version:
4.3.2 - React Version:
19.2.1 - Node Version:
22.14.0
Bug Description
Using custom numeric page sizes causes memory leaks and browser crashes in v4.3.2, even with the fixes from PR #3190 merged.
Root Cause Analysis
The Issue
In packages/layout/src/page/isHeightAuto.ts, the function checks page.box?.height:
// File: packages/layout/src/page/isHeightAuto.ts
import isNil from '../utils/isNil';
const isHeightAuto = (page: SafePageNode) => isNil(page.box?.height);
export default isHeightAuto;This function is called during pagination in packages/layout/src/page/resolvePagination.ts:
// File: packages/layout/src/page/resolvePagination.ts
import isHeightAuto from './isHeightAuto';
const resolvePagination = (doc: SafeDocumentNode) => {
const pages = doc.children || [];
for (const page of pages) {
if (isHeightAuto(page)) { // ❌ Bug triggers here!
// Pagination logic that can loop infinitely
splitPage(page);
}
}
return doc;
};The Problem
When using custom page sizes without explicit height or with height: 'auto' (e.g., size={{ width: 170.3 }}):
- Step 1: Only width is set in
page.stylebyresolvePageSizes - Step 2:
page.box.heightshould be populated byresolveDimensionsafter Yoga layout calculations - Step 3:
resolvePaginationcallsisHeightAuto()beforepage.box.heightis set - Result: Since
page.box.heightis undefined,isHeightAuto()returnstrue, triggering infinite pagination loops
Layout Pipeline Order
From packages/layout/src/index.ts:
const layout = asyncCompose(
resolveZIndex,
resolveOrigins,
resolveAssets,
resolvePagination, // ❌ Calls isHeightAuto() here
resolveTextLayout,
resolvePercentRadius,
resolveDimensions, // ✅ Sets page.box.height here (too late!)
resolveSvg,
resolveAssets,
resolveInheritance,
resolvePercentHeight,
resolvePagePaddings,
resolveStyles,
resolveLinkSubstitution,
resolveBookmarks,
resolvePageSizes, // ✅ Sets page.style.height here (first)
resolveYoga,
);The race condition: resolvePagination runs before resolveDimensions, so page.box.height is still undefined when checked!
Reproduction
Minimal Example (Triggers Bug)
import { PDFViewer, Document, Page, Text } from '@react-pdf/renderer';
function QRCodePreview() {
return (
<PDFViewer style={{ width: 240, height: 410 }}>
<Document>
<Page size={{ width: 170.3 }}> {/* ❌ No height - triggers bug */}
<Text>Hello World</Text>
</Page>
</Document>
</PDFViewer>
);
}Alternative Bug Triggers
// Using style height: 'auto'
<Page size={{ width: 170.3 }} style={{ height: 'auto' }}>
<Text>Hello World</Text>
</Page>
// Custom height with many children (pagination needed)
<Page size={{ width: 170.3, height: 200 }}>
{/* If content exceeds 200pt, pagination logic runs and checks isHeightAuto() */}
{Array.from({ length: 50 }).map((_, i) => (
<Text key={i}>Line {i}</Text>
))}
</Page>Result
- Browser hangs
- Memory usage spikes
- Eventually crashes with
RangeErroror browser becomes unresponsive
Working Example (Standard Sizes)
<Page size="A4"> {/* ✅ Works fine */}
<Text>Hello World</Text>
</Page>Expected Behavior
Custom page sizes should work identically to standard sizes like 'A4', 'Letter', etc.
Proposed Fix
Option 1: Check Both Locations
Modify packages/layout/src/page/isHeightAuto.ts:
const isHeightAuto = (page: SafePageNode) =>
isNil(page.box?.height) && isNil(page.style?.height);Option 2: Prioritize Style Height
const isHeightAuto = (page: SafePageNode) => {
const height = page.box?.height ?? page.style?.height;
return isNil(height);
};Option 3: Use setNodeHeight Logic
Apply the same logic from resolveDimensions.ts line 77-80:
// File: packages/layout/src/node/resolveDimensions.ts (existing working code)
const setNodeHeight = (node: SafeNode) => {
const value = isPage(node) ? node.box?.height : node.style?.height; // ✅ Checks both!
return setHeight(value);
};Apply this pattern to isHeightAuto.ts:
// File: packages/layout/src/page/isHeightAuto.ts (proposed fix)
import isNil from '../utils/isNil';
const isHeightAuto = (page: SafePageNode) => {
const height = page.box?.height ?? page.style?.height; // Check both locations
return isNil(height);
};
export default isHeightAuto;Related Issues
- User from PR fix: yoga error and infinite loop while page breaking #3190 reported same issue: fix: yoga error and infinite loop while page breaking #3190 (comment)
- PR fix: yoga error and infinite loop while page breaking #3190: "Fix yoga error Invalid array length at Array.push()" - Fixed general infinite loops but not this specific case
- PR fix: prevent infinite loop while splitting pages #2822: "Prevent infinite loop while splitting pages" - Earlier fix for different infinite loop scenario
Additional Context
This bug only affects custom numeric page sizes. Standard page sizes work because they follow a different code path that doesn't encounter this timing issue.
Impact: Affects any application using QR codes, labels, or custom-sized documents that don't match standard page sizes.