Understanding Client-Side PDF Generation With JavaScript
You try to generate a PDF in the browser using a library like jsPDF or pdfmake, and it kind of works until it doesn't. The errors are vague, the output looks wrong, or it crashes the tab entirely. I've been wrestling with this for years across different projects. Let me walk through what actually happens and how to fix the stuff that breaks most often. When I first started dealing with JavaScript PDF generation, I was using jsPDF for a reporting dashboard. Everything looked fine on my machine. Then someone tried it on Chrome on Windows and the Chinese characters came out as rectangles. That's the font embedding problem. The default Helvetica font in jsPDF doesn't contain CJK glyphs. I switched to loading Noto Sans SC at runtime through a base64 data URL and remapped the font family in the doc.setFont() call. Took me about twenty minutes to figure out. Not rocket science, but easy to miss if you aren't tracking font support explicitly. Another thing that caught me off guard: the size limits. jsPDF uses canvas under the hood for image insertion. Once your canvas hits around 8 megapixels, browsers start choking. I had a report that pulled in high-res screenshots from a charting library. The PDF was fine at first, but after inserting six full-width charts, the browser tab crashed consistently. I ended up downsampling the images to 96 DPI before feeding them into the PDF, which cut the file size from about 4.2 MB down to roughly 800 KB without visible quality loss on screen. You do lose some fidelity, but nobody prints these reports anyway.
What about memory leaks? A lot of people don't talk about this. Every time you create a new jsPDF instance, the library attaches event listeners and internal buffers. If you're generating PDFs in a loop or in a React component that re-mounts frequently, you will leak memory. I discovered this when a dashboard started using 400 MB of RAM after generating five PDFs over an hour. The fix was simple: destroy the instance explicitly and null out the reference, or better yet, use a singleton pattern for the PDF doc object and just clear pages between exports. That dropped memory usage to about 40 MB steady state. Then there's the issue with fonts and special characters beyond CJK. Accented characters, mathematical symbols, emoji — they all behave differently across libraries. pdfmake handles Unicode decently out of the box because it bundles a Unicode-capable font. jsPDF requires manual font registration. I spent an afternoon once trying to get the euro symbol to render correctly in a financial report. Turned out the TTF I was loading was missing the glyph. Swapped to a different weight of the same font family and it worked. Check your font files with a tool like fontforge or just query them in a browser dev console to see what's actually there. Don't assume a font file supports what it claims. Server-side generation is the alternative when client-side hits a wall. Puppeteer or Playwright can render a full page and export to PDF with correct fonts, images, and CSS. The tradeoff is you need a Node.js environment and the PDF is generated on the server, not in the browser. For most apps this is fine. But if you have strict privacy requirements or need offline generation, you're stuck with the client-side limitations. I've seen teams try to hybridize — generate the skeleton on the server, finalize with client-side edits. That introduces version mismatch problems between the Node.js PDF engine and whatever the browser is doing. Stick to one path if you can.
Here's a practical scenario. You have a table with thousands of rows and you want to paginate it across multiple PDF pages. jsPDF doesn't handle auto-pagination well. You have to calculate row heights, chunk the data, and insert page breaks manually. I wrote a helper function that takes an array of objects, a column config, and a max rows-per-page value. It returns an array of page content objects. Each object has the rows and the calculated height. You iterate through them and call doc.addPage() after each chunk. The function took me about three hours to get right, including edge cases where a single row is taller than a page. That edge case happens when you have long text fields. I just truncate those with an ellipsis and add a note on the page footer. It's not elegant, but it works. Debugging PDF output is painful because you can't easily inspect the rendered result in the browser. I recommend using the built-in preview capability of jsPDF by calling output('dataurlnewwindow'). It opens the PDF in a new tab. Add this at key points in your generation logic to verify intermediate states. Also log the dimensions of each page. If your content overflows, you'll see the discrepancy between expected and actual page height immediately. Missing this step caused me to ship a report with truncated tables once. The user noticed. Not a good experience. One more thing about performance. If you're generating large PDFs, don't do it on the main thread. It blocks the UI. Use a Web Worker. jsPDF can be loaded in a worker, and you can pass the data to it via postMessage. The worker generates the PDF and sends back a blob. This keeps the interface responsive. I implemented this for a bulk export feature that generated up to twenty PDFs per session. Without the worker, the UI froze for several seconds per generation. With it, the user could continue working while the PDFs were being created in the background. The worker approach also isolates errors, so a crash in the PDF generation doesn't take down the entire page.
Get the Full Details

If you run into issues with image compression in jsPDF, check the JPEG quality setting. The default is around 0.92, which is fine for photos but overkill for screenshots and charts. Dropping it to 0.75 usually maintains acceptable quality while reducing file size by thirty to forty percent. You can also switch to PNG for line art and charts. PNG is lossless and often smaller for graphic content. jsPDF supports both formats. The tradeoff is PNG files can be larger for photographic content. Know what your images are and pick accordingly. Font embedding is non-negotiable for production. Don't rely on system fonts. They vary across operating systems and browsers. Bundle the fonts you need as base64 or fetch them from a CDN at runtime. If you fetch at runtime, cache them locally using the Cache API or localStorage to avoid repeated downloads. This adds complexity but pays off when your users are on different devices. I learned this the hard way when a client reported that the PDF looked wrong on their Mac. The font I was relying on wasn't installed there. Swapping to a bundled font fixed it immediately. Finally, know when to stop. There are limitations to client-side PDF generation. If your use case involves complex layouts, dynamic styling, or very large documents, consider whether the effort is worth it. Sometimes a server-side solution or a third-party API is the right answer. I've seen teams spend weeks building a custom client-side PDF engine only to replace it with a simpler solution later. Don't fall into that trap. Evaluate the requirements honestly and pick the tool that fits, even if it's not the one you wanted to use.