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-by-feature comparison of jsPDF and Renderfy
FeaturejsPDF (.html())Renderfy
Where it runsIn the user's browserOn our servers — one HTTPS POST
How HTML becomes a PDF.html() rasterises the DOM via html2canvas, then embeds a bitmapHeadless Chromium prints the page — the same engine as Ctrl+P
Text in the outputPixels — not selectable, not searchable, not copyableReal vector text — selectable, searchable, indexable
CSS supportPartial. External stylesheets are frequently dropped; z-index is ignoredFull Chromium CSS — flexbox, grid, variables, @media print
Page breaksManual maths; wide layouts spill across pagesbreak-inside / break-after honoured by the print engine
Fonts14 standard PDF fonts; anything else needs base64 VFS embeddingAny Google Font by family name, per request
DeterminismVaries by browser, OS, device pixel ratio and installed fontsIdentical output for identical input, every time
Client bundle costjsPDF + html2canvas shipped to every visitorZero — nothing runs on the client
Trustworthy for invoicesNo — the user's machine produces the document and can alter itYes — generated server-side from your data
File sizeLarge — a full-page bitmap per pageSmall — text and vectors, images only where you put them
Works with no networkYesNo — it is an API call
PriceFree (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.