<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John Soto</title>
    <description>The latest articles on DEV Community by John Soto (@johnsoto90).</description>
    <link>https://dev.to/johnsoto90</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1197438%2F55a31954-381a-4868-81ab-8fed6a87dfa5.jpg</url>
      <title>DEV Community: John Soto</title>
      <link>https://dev.to/johnsoto90</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/johnsoto90"/>
    <language>en</language>
    <item>
      <title>I replaced Puppeteer with a Zig library and went from 18 to 17,000 PDFs per second</title>
      <dc:creator>John Soto</dc:creator>
      <pubDate>Wed, 15 Apr 2026 01:40:21 +0000</pubDate>
      <link>https://dev.to/johnsoto90/i-replaced-puppeteer-with-a-zig-engine-and-went-from-18-to-17000-pdfs-per-second-3ga5</link>
      <guid>https://dev.to/johnsoto90/i-replaced-puppeteer-with-a-zig-engine-and-went-from-18-to-17000-pdfs-per-second-3ga5</guid>
      <description>&lt;p&gt;If you've worked with designers, you know the routine.&lt;/p&gt;

&lt;p&gt;They hand you PDF templates: invoices, reports, certificates. Specific fonts, aligned columns, logos.&lt;/p&gt;

&lt;p&gt;You look at it and think: "I could build this in HTML and CSS in 20 minutes."&lt;/p&gt;

&lt;p&gt;So you do. Puppeteer. &lt;code&gt;page.pdf()&lt;/code&gt;. Perfect output.&lt;/p&gt;

&lt;p&gt;Then reality hits.&lt;/p&gt;

&lt;p&gt;A client clicks "Download All" on 5,000 invoices and your server melts.&lt;/p&gt;

&lt;p&gt;I optimized everything I could. Browser warmup, page reuse, connection pooling. Best I got was about 25 PDFs per second.&lt;/p&gt;

&lt;p&gt;At some point you realize: you can't optimize around running a full browser engine to generate a static document.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I tried something different
&lt;/h2&gt;

&lt;p&gt;I'm a Bun user, and Bun is fast partly because it's built in Zig. I'd been wanting to learn Zig, and a PDF renderer felt like the perfect project: real problem, clear spec, and a chance to see what the language could actually do.&lt;/p&gt;

&lt;p&gt;So I started building. Just a &lt;code&gt;&amp;lt;Page&amp;gt;&lt;/code&gt; and a &lt;code&gt;&amp;lt;Box&amp;gt;&lt;/code&gt;. If I could render that to a valid PDF, I had something.&lt;/p&gt;

&lt;p&gt;Then I kept going. Styles, text wrapping, fonts with subsetting, images, QR codes. Each feature was a small, solvable problem. I paired with Claude, not to generate code blindly, but to work through the hard parts: the PDF spec, font parsing, AES encryption, layout edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@slothpdf/render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  &amp;lt;Page size="A4" margin="20mm"&amp;gt;
    &amp;lt;Box class="text-2xl font-bold mb-4"&amp;gt;{name}&amp;lt;/Box&amp;gt;
    &amp;lt;Box class="text-sm text-gray-600"&amp;gt;{description}&amp;lt;/Box&amp;gt;
  &amp;lt;/Page&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No browser. No runtime overhead. Just template in, PDF out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4arpyuxvrqb8ud9j8j4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4arpyuxvrqb8ud9j8j4.gif" alt="SlothPDF rendering demo" width="760" height="478"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Batch&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`invoices/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pdf`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Merge into one multi-page PDF&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// ZIP archive&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// AES-256 encryption&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All options stack. Not a full HTML/CSS engine. Intentionally limited to predictable layouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;p&gt;Apple M4, single-threaded, 1,000 unique invoices with custom fonts and 3-10 line items per invoice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SlothPDF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17,000 /sec&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jsPDF&lt;/td&gt;
&lt;td&gt;7,750 /sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Puppeteer&lt;/td&gt;
&lt;td&gt;~18 /sec&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Memory stays flat. 50,000 renders, no growth. 1,000 PDFs in ~60ms on a single thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's fast
&lt;/h2&gt;

&lt;p&gt;There's no trick. It's just fewer layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No browser, no DOM, no CSS parser&lt;/li&gt;
&lt;li&gt;Fonts are parsed once and cached for the process lifetime&lt;/li&gt;
&lt;li&gt;Templates are parsed once, only data binding and layout run per render&lt;/li&gt;
&lt;li&gt;Memory is allocated in arenas that reset after each PDF, no GC&lt;/li&gt;
&lt;li&gt;The Zig engine writes PDF bytes directly, no intermediate format&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add @slothpdf/render
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://slothpdf.jsoto.cloud/editor" rel="noopener noreferrer"&gt;Playground&lt;/a&gt; (build templates in the browser)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://npmjs.com/package/@slothpdf/render" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/voidzer0-dev/slothpdf-render" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Requires Bun 1.1+. macOS and Linux.&lt;/p&gt;

&lt;p&gt;If you're hitting Puppeteer limits, this removes the bottleneck entirely. Curious what you're using for PDFs today.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>javascript</category>
      <category>zig</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
