jsPDF Alternative
Renderfy vs jsPDF
jsPDF is an excellent PDF writer. It is not an HTML renderer. The moment you call .html(), your document becomes a screenshot in a PDF wrapper — blurry, unsearchable and different on every device.
The one thing that explains every jsPDF bug you have hit
jsPDF has two completely different modes, and almost every complaint about it comes from confusing them.
The drawing API — doc.text(), doc.rect()
Real vector text, small files, fully searchable. But you position everything by coordinate. There is no layout engine, so a table with variable-length rows is yours to compute.
doc.html() — the convenience path
Delegates to html2canvas, which repaints the DOM onto a canvas, and embeds the result as a bitmap. This is why the text is not selectable, the file is heavy, z-index is ignored, and wide layouts spill off the page.
There is no jsPDF option that fixes this, because there is nothing to fix — the library is doing exactly what it says. Getting real text out of real CSS requires a real layout engine, and the only ones that exist are browsers. Renderfy runs one for you.
Feature comparison
| Feature | jsPDF (.html()) | Renderfy |
|---|---|---|
| Where it runs | In the user's browser | On our servers — one HTTPS POST |
| How HTML becomes a PDF | .html() rasterises the DOM via html2canvas, then embeds a bitmap | Headless Chromium prints the page — the same engine as Ctrl+P |
| Text in the output | Pixels — not selectable, not searchable, not copyable | Real vector text — selectable, searchable, indexable |
| CSS support | Partial. External stylesheets are frequently dropped; z-index is ignored | Full Chromium CSS — flexbox, grid, variables, @media print |
| Page breaks | Manual maths; wide layouts spill across pages | break-inside / break-after honoured by the print engine |
| Fonts | 14 standard PDF fonts; anything else needs base64 VFS embedding | Any Google Font by family name, per request |
| Determinism | Varies by browser, OS, device pixel ratio and installed fonts | Identical output for identical input, every time |
| Client bundle cost | jsPDF + html2canvas shipped to every visitor | Zero — nothing runs on the client |
| Trustworthy for invoices | No — the user's machine produces the document and can alter it | Yes — generated server-side from your data |
| File size | Large — a full-page bitmap per page | Small — text and vectors, images only where you put them |
| Works with no network | Yes | No — it is an API call |
| Price | Free (MIT) | Credit-based from $9/mo — 3 credits per PDF |
The migration
Before — client-side
import jsPDF from "jspdf";
import html2canvas from "html2canvas";
async function download() {
const el = document.getElementById("invoice")!;
const canvas = await html2canvas(el, { scale: 2 });
const img = canvas.toDataURL("image/png");
const doc = new jsPDF("p", "mm", "a4");
doc.addImage(img, "PNG", 0, 0, 210, 297);
doc.save("invoice.pdf");
}
// Two libraries shipped to every visitor.
// Output: a bitmap. No selectable text.
// Looks different in Safari.After — server-side
// app/api/invoice/route.ts
export async function POST(req: Request) {
const invoice = await req.json();
const res = await fetch(
"https://renderfy.io/api/v1/render",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RENDERFY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "html",
content: renderInvoiceHtml(invoice), // your markup
options: { format: "pdf", dimensions: "a4" },
}),
}
);
return new Response(await res.arrayBuffer(), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition":
'attachment; filename="invoice.pdf"',
},
});
}
// 0 KB of client JS. Vector text.
// Byte-identical on every device.Your existing markup is the payload. The work is moving the render behind an endpoint and deleting two client dependencies.
When to keep jsPDF
- → The document must be generated offline, with no network round trip.
- → The data is sensitive enough that it must never leave the user's device.
- → You are drawing a fixed layout with the coordinate API, not converting HTML — that path produces genuinely good PDFs.
- → Zero marginal cost matters more than fidelity, and the output is a throwaway.
Frequently asked questions
Why is my jsPDF output blurry?+
Because jsPDF .html() does not lay out your HTML as a document. It hands the element to html2canvas, which paints an approximation of the DOM onto a canvas, and jsPDF embeds that canvas as a raster image. You are looking at a screenshot inside a PDF wrapper, so it blurs when zoomed or printed. Raising the scale option makes the file larger without making the text vector.
Why can I not select the text in a jsPDF-generated PDF?+
Same root cause. When the PDF is produced through .html(), the text is pixels in a bitmap, not glyphs in a font. Nothing downstream — search, copy-paste, accessibility readers, indexing, PDF/A compliance — can see it as text. Text drawn with the low-level doc.text() API is real text, but then you are laying the document out by coordinate rather than with CSS.
Why does my CSS not apply in jsPDF?+
Two separate problems. External stylesheets passed to .html() are often not resolved, so you need styles inline or in a <style> block on the element. And html2canvas paints elements in DOM order rather than by stacking context, so z-index has no effect — overlapping elements come out in the wrong order.
Is there a drop-in replacement for jsPDF?+
Not a drop-in one, because the model is different: jsPDF is a client library and Renderfy is a server API. But the migration is usually smaller than expected — you delete the jsPDF and html2canvas imports, POST the same HTML string to one endpoint, and get a PDF binary back. The HTML and CSS you already wrote are the payload.
When should I keep using jsPDF?+
Keep it when the document must be produced offline, when the data must never leave the user's device for privacy or regulatory reasons, or when you are drawing a simple fixed layout programmatically with doc.text() and doc.rect() rather than converting HTML. Those are cases an API cannot serve.
Does Renderfy work from a browser?+
It should not be called from a browser, because that would expose your API key. Call it from a route handler, serverless function, or backend service and stream the PDF to the user. The @renderfy/sdk client is zero-dependency and supports Node.js 18+, Next.js Edge, Cloudflare Workers and Bun; any other language can call the endpoint directly.
Related
Vector text, in one POST
100 free credits, no card required. Send the HTML you already have and get a real PDF back.