<?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: Rizwan</title>
    <description>The latest articles on DEV Community by Rizwan (@rizwan_d277f60ad55734c55c).</description>
    <link>https://dev.to/rizwan_d277f60ad55734c55c</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4240605%2Fd348ca6d-b30e-4244-8627-bc7bec2372ac.jpg</url>
      <title>DEV Community: Rizwan</title>
      <link>https://dev.to/rizwan_d277f60ad55734c55c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rizwan_d277f60ad55734c55c"/>
    <language>en</language>
    <item>
      <title>How I Built a 52-Tool Client-Side PDF Suite with WebAssembly, OPFS, and $0 Server Costs</title>
      <dc:creator>Rizwan</dc:creator>
      <pubDate>Thu, 12 Nov 2026 08:41:20 +0000</pubDate>
      <link>https://dev.to/rizwan_d277f60ad55734c55c/how-i-built-a-52-tool-client-side-pdf-suite-with-webassembly-opfs-and-0-server-costs-4p2g</link>
      <guid>https://dev.to/rizwan_d277f60ad55734c55c/how-i-built-a-52-tool-client-side-pdf-suite-with-webassembly-opfs-and-0-server-costs-4p2g</guid>
      <description>&lt;p&gt;Online PDF tools have an uncomfortable default: to merge two bank statements or shrink a 3 MB certificate to 180 KB, you are expected to upload private identity documents to someone else's cloud server.&lt;/p&gt;

&lt;p&gt;I wanted to know how far modern browser APIs could push that entire workflow locally—with &lt;strong&gt;$0 in backend compute&lt;/strong&gt;, zero S3 storage, and zero server uploads. That experiment grew into &lt;strong&gt;&lt;a href="https://everydaypdf.me" rel="noopener noreferrer"&gt;EverydayPDF&lt;/a&gt;&lt;/strong&gt;, a static Next.js 14 application hosting 52 client-side PDF and document tools on Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;Here is the architecture behind running 52 PDF utilities strictly in browser memory, and the real-world bottlenecks I ran into along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Static Export First: Forcing Zero-Backend Discipline
&lt;/h2&gt;

&lt;p&gt;The app is configured in Next.js 14 App Router with &lt;code&gt;output: 'export'&lt;/code&gt;. Every route compiles down to static HTML, CSS, and client-side JavaScript bundles in &lt;code&gt;./out&lt;/code&gt;. Because there is no Node.js API route or upload server behind the scenes, it is architecturally impossible for a tool to sneak a file off the user's machine.&lt;/p&gt;

&lt;p&gt;The core stack relies on four client-side pillars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pdf-lib&lt;/code&gt; &amp;amp; &lt;code&gt;pdf-lib-encrypt&lt;/code&gt;&lt;/strong&gt; for vector page-tree operations (merging, splitting, rotating, flattening AcroForms, adding page numbers, and AES-256 encryption).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mozilla &lt;code&gt;pdfjs-dist&lt;/code&gt;&lt;/strong&gt; for parsing PDF streams and rendering thumbnails or high-DPI canvases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Tesseract.js&lt;/code&gt; (WebAssembly)&lt;/strong&gt; for client-side OCR so scanned PDFs get a real &lt;code&gt;Ctrl+F&lt;/code&gt; searchable text layer locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML5 Canvas + &lt;code&gt;jszip&lt;/code&gt; / &lt;code&gt;xlsx&lt;/code&gt;&lt;/strong&gt; for image calibration, table extraction, and document bundling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Keeping the UI at 60 FPS with Transferable Web Workers
&lt;/h2&gt;

&lt;p&gt;Parsing and copying hundreds of PDF page objects on the main UI thread freezes scroll and click interactions. To prevent jank, heavy binary tasks like merging and splitting run inside a dedicated background worker (&lt;code&gt;workers/pdf.worker.ts&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Instead of structured-cloning large &lt;code&gt;Uint8Array&lt;/code&gt; payloads back to the main thread (which doubles peak RAM usage), the worker transfers ownership of the underlying &lt;code&gt;ArrayBuffer&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;mergedBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mergedPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mergedBytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'SUCCESS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This zero-copy handoff keeps memory spikes tight even on mid-range laptops and mobile browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Handling Large Files via OPFS (Origin Private File System)
&lt;/h2&gt;

&lt;p&gt;Standard &lt;code&gt;File.arrayBuffer()&lt;/code&gt; calls work fine for 5 MB PDFs, but holding multiple 40 MB scanned documents simultaneously in JavaScript heap memory can crash mobile browser tabs.&lt;/p&gt;

&lt;p&gt;When combined input sizes cross 30 MB in tools like &lt;a href="https://everydaypdf.me/merge-pdf" rel="noopener noreferrer"&gt;Merge PDF&lt;/a&gt;, EverydayPDF falls back to the browser's &lt;strong&gt;Origin Private File System (OPFS)&lt;/strong&gt; (&lt;code&gt;navigator.storage.getDirectory()&lt;/code&gt;), staging large file streams in sandboxed local browser storage and reading buffers sequentially rather than pinning every file in heap RAM at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Solving High-Intent Real-World Constraints
&lt;/h2&gt;

&lt;p&gt;Building generic "Merge" and "Split" pages wasn't enough—users repeatedly hit hyperspecific formatting walls on government, academic, and banking portals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict Byte Ceilings:&lt;/strong&gt; Portals like UPSC, SSC, and GATE reject certificates above 200 KB, 100 KB, or 50 KB. Naive compression destroys roll numbers and seals. In &lt;a href="https://everydaypdf.me/compress-pdf-under-200kb" rel="noopener noreferrer"&gt;Compress PDF Under 200KB&lt;/a&gt;, a multi-pass canvas loop adjusts DPI scaling and JPEG chroma quality per page budget until the output lands strictly below the target KB threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Photo &amp;amp; Signature Calibration:&lt;/strong&gt; The &lt;a href="https://everydaypdf.me/photo-signature-resizer" rel="noopener noreferrer"&gt;Photo &amp;amp; Signature Resizer&lt;/a&gt; handles exact pixel/centimeter targets (&lt;code&gt;3.5 × 4.5 cm&lt;/code&gt;), &lt;code&gt;20–50 KB&lt;/code&gt; byte targets, paper-background whitening for phone-photographed signatures, and optional candidate name/date stamps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Printable Imposition &amp;amp; Banking Privacy:&lt;/strong&gt; Tools like &lt;a href="https://everydaypdf.me/id-card-to-a4" rel="noopener noreferrer"&gt;Admit Card / ID Card 2-in-1 A4&lt;/a&gt; tile front-and-back IDs with dashed cutting lines, while &lt;a href="https://everydaypdf.me/unlock-and-merge-bank-statements" rel="noopener noreferrer"&gt;Unlock &amp;amp; Merge Bank Statements&lt;/a&gt; decrypts a year's worth of password-locked statements locally before users pass them to &lt;a href="https://everydaypdf.me/redact-pdf" rel="noopener noreferrer"&gt;Auto-Redact PII&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Connecting Tools with In-App Workflow Chaining
&lt;/h2&gt;

&lt;p&gt;One subtle UX lesson: PDF tasks rarely happen in isolation. Someone who finishes merging 6 documents almost always needs to compress the bundle under a portal limit, add page numbers, or lock it with a password.&lt;/p&gt;

&lt;p&gt;Rather than forcing users back to the homepage, a central workflow graph (&lt;code&gt;lib/workflow-chains.ts&lt;/code&gt;) maps every one of the 52 tool routes to 3 logical next-step actions displayed right after download completion—turning single-tool visits into smooth multi-step document workflows while keeping every byte inside the browser.&lt;/p&gt;

&lt;p&gt;If you want to inspect the client-side workflows in action, try out the live suite at &lt;strong&gt;&lt;a href="https://everydaypdf.me" rel="noopener noreferrer"&gt;EverydayPDF&lt;/a&gt;&lt;/strong&gt;—especially &lt;a href="https://everydaypdf.me/compress-pdf-under-200kb" rel="noopener noreferrer"&gt;Compress PDF Under 200KB&lt;/a&gt; or &lt;a href="https://everydaypdf.me/ocr-pdf" rel="noopener noreferrer"&gt;OCR to Searchable PDF&lt;/a&gt;—and let me know what edge cases you'd tackle next in client-side WebAssembly!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>webassembly</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
