npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

turbo-html2pdf

v0.1.7

Published

Fast native HTML/CSS + Jinja templating to PDF for Node — no headless browser, no Chromium.

Readme

turbo-html2pdf

Turn HTML + CSS (with a Jinja templating layer) into a PDF — natively, in Rust, with no headless browser. A from-scratch document engine: templating → HTML/CSS layout → automatic pagination → PDF, shipped as a tiny native addon for Node and WebAssembly. No Chromium download, no 200 ms browser spin-up, deterministic output.

npm: turbo-html2pdf (Node) · turbo-html2pdf-wasm (browser) · turbo-html2pdf-react / turbo-html2pdf-template (authoring) · turbo-html2pdf on PyPI (Python). The name says exactly what it does — HTML → PDF — and avoids clashing with the unrelated "TurboPDF".

🌐 It generates PDFs entirely in the browser

Because the engine is pure Rust → WebAssembly (~3 MB), it runs 100% client-side — no server, no backend, no Chromium. A user pastes HTML/CSS, supplies a font as a Uint8Array, and gets PDF bytes, all in the browser tab.

As far as we know, no other library does HTML/CSS → PDF in the browser: the HTML→PDF tools that match its fidelity (Puppeteer, Playwright, Gotenberg, WeasyPrint, wkhtmltopdf) are all server-side — they drive a headless browser or a native binary you have to host. The libraries that do run in the browser (jsPDF, PDFKit, react-pdf) are draw APIs, not HTML/CSS layout engines — you place every box by hand. turbo-html2pdf is the only one that is both a real HTML/CSS engine and runs with zero server.

import init, { compile } from 'turbo-html2pdf-wasm'
await init()                                  // load the ~3 MB wasm once
const program = compile('<h1>{{ t }}</h1>')
const { pdf } = program.render({ data: { t: 'Hello' }, css: 'h1{font-size:24pt}',
                                 fonts: [{ data: fontBytes, family: 'Inter' }] })
// `pdf` is a Uint8Array — download it, no round-trip to a server

Why it's fast

It does HTML/CSS → PDF without a browser. The incumbent way to turn HTML into a PDF is to drive headless Chrome (Puppeteer/Playwright/Gotenberg) — which means shipping ~200 MB of Chromium, paying a per-process spin-up, and a big memory footprint. turbo-html2pdf is a native layout + PDF engine instead, so the common "render one document" path is tens to hundreds of times faster.

Benchmarks (this machine: Apple M3, 8 cores; benches/competitive)

Two scenarios, both measured:

  • Warm (server): the realistic server path — the template's compiled Program is reused and fonts are parsed once and cached (the Fonts handle). This is the number to lead with.
  • Cold (one-shot): a single render from a cold start (compile + parse fonts + render), e.g. a CLI invocation.

All engines render the same content (same fonts, A4 geometry, same text); a PNG-equivalence diff (sim ≈ 0.98 vs the reference) confirms the outputs are comparable, so this isn't "winning by doing less".

invoice — a one-page document (the bread-and-butter case), median ms:

| Engine | Warm (cached) | Cold | vs turbo (warm) | Memory | Ships a browser? | |---|--:|--:|--:|--:|:--:| | turbo-html2pdf | 1.27 | 1.48 | | 232 MB | no | | Playwright (Chromium) | 46.3 | 49.4 | 36× slower | 393 MB | yes (~200 MB) | | Gotenberg (Chromium) | 66.9 | 70.6 | 53× slower | 424 MB | yes (~1 GB) | | Puppeteer (Chromium) | 86.1 | 132.2 | 68× slower | 550 MB | yes (~200 MB) | | Typst (native, own DSL) | 63.8 | 62.7 | 50× slower | 1.5 GB | no | | WeasyPrint (Python) | 375.8 | 379.3 | 296× slower | 379 MB | no |

It's also the fastest on every other workload — not just the small one:

| Workload | turbo-html2pdf (warm/cold) | Playwright | Puppeteer | Gotenberg | WeasyPrint | Typst | |---|--:|--:|--:|--:|--:|--:| | invoice (1 page) | 1.27 / 1.48 | 46 / 49 | 86 / 132 | 67 / 71 | 376 / 379 | 64 / 63 | | report-1k (1 000-row table) | 119 / 130 | 187 / 170 | 384 / 429 | 204 / 204 | 2 550 / 2 547 | 220 / 221 | | legal (prose + footnotes) | 25 / 26 | 71 / 98 | 124 / 147 | 80 / 80 | 444 / 453 | 72 / 72 | | mixed (flex + table) | 8.6 / 9.2 | 57 / 58 | 94 / 229 | 73 / 73 | 432 / 437 | 69 / 71 |

Against every HTML/CSS-to-PDF engine, turbo-html2pdf wins on all workloads — by 1.6× (big tables) up to ~300× (small docs) — at lower memory and with no browser to install.

Honest footnote. pdfkit (~6 ms) and jspdf (~2 ms) are faster on raw output, but they are imperative draw APIsyou compute and place every box; there is no HTML/CSS, no layout engine, no pagination. @react-pdf is a React component DSL (no HTML/CSS) and its flexbox layout melts on big tables (report-1k1 300 ms, ~10× slower than turbo). Different category from "give me HTML, get a PDF". wkhtmltopdf (legacy/unmaintained) was not installed.

Numbers are "on this machine", never absolutes — rerun with cd benches/competitive && pnpm bench. Full table + methodology in benches/competitive/RESULTS.md.

Why it's that fast

  • No browser, no process spin-up. It's a library call, not an IPC round-trip to Chrome.
  • The font program is parsed once. ttf-parser + rustybuzz faces are cached in the FontFace (via self_cell) instead of re-parsed per text run; with the Fonts warm-start handle a server parses fonts once at startup and reuses them.
  • Box styles resolve once per box. Context-independent boxes cache their resolved BoxStyle instead of re-parsing ~25 CSS properties on every layout pass.
  • Shaping is memoized by run text (the measure + place passes, and repeated cells, share one shape).
  • Deterministic. No wall clock, no randomness, no system-font lookup — identical inputs produce byte-identical PDFs.

Architecture

A template is compiled once into a reusable Program, then rendered against data many times. The pipeline (crates/turbo-pdf-core):

template (HTML + Jinja + t: directives)
   │  compile()                      ── parse + cache (MiniJinja)
   ▼
render_pages(data, css, fonts):
   │  1. render markup               ── Jinja → HTML string (data substituted)
   │  2. parse                       ── html5ever → node tree, t: directives typed
   │  3. cascade + inherit           ── CSS subset → ComputedStyle per node
   │  4. layout                      ── box tree → block / inline / flex (taffy) / table
   │                                    → a "galley" of positioned, shaped fragments
   │  5. paginate                    ── break the galley into pages (orphans/widows,
   │                                    break rules, repeated <thead>); resolve
   │                                    running headers/footers + footnotes per page
   ▼  emit_pdf()                     ── fragments → PDF 1.7: subset + embed fonts
                                        (TrueType / Type0-CFF), text, vector boxes/
   PDF bytes                            borders, raster images, watermark

Layout, font shaping (rustybuzz), and PDF writing (pdf-writer + subsetter) are all native; taffy powers flexbox. The engine embeds no fonts and does no network or filesystem I/O — fonts and images are supplied by the caller.

Frontends & bindings (packages/, crates/):

  • turbo-html2pdf — Node native addon (compile/renderBuffer).
  • turbo-html2pdf-wasm — the same engine in the browser (WebAssembly).
  • turbo-html2pdf-react — author templates as React components (compiled to the template string at build time, never on the render path).
  • turbo-html2pdf-template — author templates with plain functions (no React).

The engine is Send + Sync: one compiled Program renders concurrently across threads.


The DSL — HTML for documents

You write ordinary HTML + a CSS subset, plus a Jinja templating layer for data, plus a handful of t: directives for paged-media features browsers can't do. Full reference in docs/: dsl.md, paged-media.md, css-support.md, api.md.

  • Templating (Jinja / MiniJinja): {{ value }}, {% for %}, {% if %}, includes/macros, plus document filters (currency, number, percent, ordinal, date, datetime, …) and a {% switch %} / {% case %} extension.
  • t: directives (the paged-media layer):
    • <t:running-header> / <t:running-footer> — headers/footers repeated on every page, re-evaluated per page so {{ page.number }} / {{ page.total }} (and <t:page/> / <t:pages/>) are correct.
    • <t:footnote> — auto-numbered footnotes; the body lands on the page where the reference falls (content-driven, with a body/footnote fix-point).
    • Pagination is automatic: page count is an output, never an input. @page sets size/margins; break rules + orphans/widows are honored; <thead> repeats when a table spans pages.
const { compile, Fonts } = require('turbo-html2pdf')
const fs = require('node:fs')

// Warm at startup, reuse per request — the fast path.
const program = compile('<h1>{{ title }}</h1><p>{{ body }}</p>')
const fonts = Fonts.load([fs.readFileSync('Inter.ttf')])   // parse fonts ONCE

const { pdf, pageCount } = program.render({
  data: { title: 'Hello', body: 'World' },
  css: '@page { size: A4; margin: 20mm } h1 { font-size: 24pt }',
  meta: { title: 'My Doc' },
}, fonts)                                                   // reuse the handle

fs.writeFileSync('out.pdf', pdf)   // %PDF-1.7

Features

  • HTML + CSS subset (block / inline / flexbox / tables), automatic pagination.
  • Running headers & footers with per-page values; auto-numbered footnotes.
  • Font subsetting + embedding (TrueType & CFF/OpenType); per-glyph fallback.
  • Raster images (PNG/JPEG, alpha → SMask) with a sane max-size clamp.
  • Watermarks: out-of-the-box faded diagonal DRAFT text (word/colour/angle configurable) and background-image watermarks.
  • Optional, off-by-default capability gates: endnotes, print-color (CMYK). Planned: xref, svg, pdf-a, pdf-ua — see TODO/.
  • Deterministic output; Send + Sync; no network / no system fonts.

Status

v0.1.7. The core engine is complete and heavily tested (the turbo-pdf-core crate holds 100% line coverage with a cyclomatic-complexity ≤ 5 gate). Bindings: Node (napi) and WebAssembly. See docs/ for the full guide and benches/competitive/ for the benchmark harness.

License

MIT.