Generating PDFs on the web is usually a nightmare. You basically have two choices, and they both suck:
- Server-side: Spin up a headless Chrome (Puppeteer) on a backend. It’s 100% accurate, but slow, expensive, and breaks every time AWS updates their Lambda environment.
- Client-side (
window.print): Fast, but good luck telling the browser to ignore the navigation bar or force a page break inside a table.
I recently had to build an Invoice Generator. My budget for servers was $0.
So I went down the rabbit hole of Client-Side PDF Generation with @react-pdf/renderer.
Here is how I survived.
The Mental Shift: It's Not HTML
The first mistake I made was trying to treat it like a web page.
@react-pdf uses React components, yes. But it does not render HTML. You can't use <div> or <span>. You can't use CSS Grid (yet).
You have to think in PDF primitives:
<Document>
<Page size="A4">
<View style={styles.section}>
<Text>Hello World</Text>
</View>
</Page>
</Document>
It feels like writing React Native. Primitive UI blocks, Flexbox everywhere. If you try to use a standard HTML <img> tag, it will crash. You must use their <Image /> component.
The "Font" Panic
Everything was working great on my MacBook. Then I deployed, and the text turned into garbage symbols. Lesson Learned: PDF generators are dumb. They don't know what "Arial" or "Helvetica" is unless you explicitly feed them the font file.
Do not rely on system fonts. Fetch them from a CDN and register them explicitly.
Font.register({
family: "Open Sans",
src: "https://fonts.gstatic.com/.../OpenSans-Regular.ttf",
});
If you don't do this, your users will see boxes instead of letters. Also, load these fonts asynchronously in your internal _app.js or root component to avoid blocking the main thread.
Handling Images: Welcome to CORS Hell
Here is a scenario that wasted 2 days of my life:
You render an image in the PDF from a URL: <Image src="https://my-bucket.s3.amazonaws.com/logo.png" />.
It works in development. It breaks in production with a generic error.
Why? CORS. The PDF generator fetches the image via XHR. If your S3 bucket doesn't have the correct CORS headers allowing your domain, the request is blocked, and the PDF generation fails silently.
The Fix:
Ensure your S3/CDN headers allow Access-Control-Allow-Origin: *. Or, better yet, convert your images to Base64 strings before passing them to the PDF renderer. It bloats the payload, but it's bulletproof.
Solving the Layout Shift (Page Breaks)
HTML scrolls forever. A4 paper ends at 297mm. What happens when your table row gets cut in half at the bottom of the page? It looks unprofessional.
@react-pdf tries to guess, but often fails. You need to be explicit.
Use the break prop to force a new page, or wrap={false} to ensure a component stays together.
// This entire block will jump to the next page if it doesn't fit
<View wrap={false}>
<Text>Item Description</Text>
<Image src={productImage} />
<Text>Price: $100</Text>
</View>
Performance: The "Web Worker" Trick
Here is the UX trap: I wanted a "Live Preview". User types "Invoice #001", and the PDF updates instantly. But rendering a PDF is heavy (CPU intensive). Doing it on the main thread freezes the browser UI. The user types "h", the browser hangs for 200ms, then "e", hangs again.
The solution is two-fold:
- Aggressive Debouncing: Only render 1 second after typing stops.
- Web Workers (Advanced): Move the PDF generation logic to a Web Worker.
By default, @react-pdf/renderer runs on the main thread. But you can offload it. This keeps your UI buttery smooth (60fps) while the heavy lifting happens in the background thread.
Why It Was Worth It
In the end, I shipped a feature that generates pixel-perfect A4 invoices.
- Server Cost: $0 (It runs on the user's laptop).
- Privacy: 100% (The data never leaves their browser).
- Speed: Instant download.
If you are building an app that needs to output standard documents like certificates, invoices, or reports, don't pay for a PDF API. Let the client do the work. The fans on their laptop might spin up, but that's a price I'm willing to let them pay.