<?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: Sapianyi</title>
    <description>The latest articles on DEV Community by Sapianyi (@sapianyi).</description>
    <link>https://dev.to/sapianyi</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%2F3947985%2F2e308053-459c-46d8-b210-3bc4ce5267a9.png</url>
      <title>DEV Community: Sapianyi</title>
      <link>https://dev.to/sapianyi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sapianyi"/>
    <language>en</language>
    <item>
      <title>How I Built a Blazingly Fast, Privacy-First Batch Image Converter in the Browser Using OPFS and Web Workers</title>
      <dc:creator>Sapianyi</dc:creator>
      <pubDate>Sat, 23 May 2026 17:25:13 +0000</pubDate>
      <link>https://dev.to/sapianyi/how-i-built-a-blazingly-fast-privacy-first-batch-image-converter-in-the-browser-using-opfs-and-web-1f8p</link>
      <guid>https://dev.to/sapianyi/how-i-built-a-blazingly-fast-privacy-first-batch-image-converter-in-the-browser-using-opfs-and-web-1f8p</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Problem with Modern Web Tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most online image converters follow a flawed pattern: you upload your files to a cloud server, their backend processes them, and you download them back. If you are handling hundreds of images, this layout introduces massive network bottlenecks. More importantly, it completely breaks user data privacy.&lt;/p&gt;

&lt;p&gt;I wanted to build a batch image conveyor that processes hundreds of files instantly, supports next-gen formats (WebP, AVIF, QOI), and ensures that zero bytes of user data ever leave the local machine.&lt;/p&gt;

&lt;p&gt;But doing this purely on the client side brings two massive boss-level challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;UI Freezing: Image compression is CPU-intensive. Running it on the main thread makes the browser completely unresponsive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Out of Memory (OOM) Crashes: Keeping raw pixel arrays for 100+ high-res images in the browser's JavaScript heap will instantly crash the tab.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is how I solved this architecture using modern Web APIs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architecture: Continuous Streaming Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To bypass the browser's RAM and CPU limitations, I built a decoupled streaming architecture. Dev.to supports Mermaid.js rendering natively, so here is exactly how the data flows from your desktop back to your download folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    Input[User Input: Drag &amp;amp; Drop / Files] --&amp;gt;|Stream Raw Bytes| OPFS[(OPFS Sandbox Disk)]
    OPFS --&amp;gt;|Sequential Read| Pool[WorkerPool Manager]

    subgraph Multi-Threaded Core (Backpressure Capped &amp;lt; 200MB)
        Pool --&amp;gt;|Payload 1| W1[Worker 1: WebP/Lanczos3]
        Pool --&amp;gt;|Payload 2| W2[Worker 2: Nearest/QOI]
        Pool --&amp;gt;|Payload N| WN[Worker N: AVIF WASM]
    end

    W1 --&amp;gt;|Compressed Blobs| Zip[fflate ZIP Archiver]
    W2 --&amp;gt;|Compressed Blobs| Zip
    WN --&amp;gt;|Compressed Blobs| Zip

    Zip --&amp;gt;|Continuous Binary Stream| Download[Instant Local Download]

    style Multi-Threaded Core fill:#121214,stroke:#39ff14,stroke-width:2px
    style OPFS fill:#1f2937,stroke:#58a6ff,stroke-width:1px
    style Download fill:#065f46,stroke:#10b981,stroke-width:2px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Eliminating RAM Bloat with OPFS (Origin Private File System)&lt;br&gt;
Instead of loading dropped files directly into memory, my pipeline instantly intercepts the stream and writes the raw binary data into the Origin Private File System (OPFS) sandbox.&lt;br&gt;
OPFS acts as a fast, isolated virtual disk inside the browser. This allows the application to accept a folder with 500+ items without consuming more than a few megabytes of actual RAM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-Threading with a Custom Web Worker Pool&lt;br&gt;
To keep the frame rate at a buttery-smooth 60fps, all conversion and scaling tasks are delegated to a pool of background Web Workers. The number of active workers scales dynamically based on the user's CPU thread count (&lt;em&gt;navigator.hardwareConcurrency&lt;/em&gt;).&lt;br&gt;
Each worker loads target codecs and executes compression in total isolation from the UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implementing Strict Backpressure&lt;br&gt;
If you feed 100 workers at once, the browser will still crash due to rapid memory allocation. To counter this, I implemented a custom Backpressure mechanism inside the &lt;em&gt;WorkerPool&lt;/em&gt;.&lt;br&gt;
The pool tracks total active byte allocation. If the memory footprint of "in-flight" images approaches 200MB, the pipeline pauses reading from OPFS. As soon as a worker finishes compressing an image and releases its buffer, the pipeline pushes the next asset forward.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Streaming ZIP Archive Compilation&lt;br&gt;
Once compressed, storing finished assets back to memory to create a ZIP file would defeat the whole purpose. Instead, the architecture streams individual compressed files into &lt;em&gt;fflate&lt;/em&gt; on the fly, packaging them into a continuous Blob stream that triggers an instant local download.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why not a full WebAssembly monolith?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A common question for this kind of heavy-lifting utility is: "Why didn't you just compile a native C++ or Rust image processing library directly into a single WebAssembly (WASM) binary?"&lt;/p&gt;

&lt;p&gt;While WASM is incredibly fast, using it as a monolithic backend inside the browser has critical trade-offs for this specific architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native Browser Strengths: Modern browsers already have hyper-optimized, native, hardware-accelerated pipelines for rendering and encoding formats like WebP. Wrapping JS APIs inside a Worker Pool lets us use these native engines for free without the penalty of huge WASM binary overhead.&lt;/li&gt;
&lt;li&gt;OPFS Threading Sync: Working closely with the Origin Private File System, generating local sandboxed URLs, and handling dynamic runtime cancellations is significantly easier and safer through asynchronous JavaScript/TypeScript Workers.&lt;/li&gt;
&lt;li&gt;Hybrid Approach: Instead of a full-WASM monolith, I chose a hybrid ecosystem. JavaScript handles orchestration, pipeline state, and native codecs, while WebAssembly is injected strictly where JS is too slow — specifically inside background workers for heavy Lanczos3 image resampling (via Pica) and advanced AVIF encoding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Tech Stack Inside&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vite + TypeScript: Fast building and type-safe core pipelines.&lt;/li&gt;
&lt;li&gt;Pica: Industrial-grade Lanczos3 resampling filter (WASM accelerated).&lt;/li&gt;
&lt;li&gt;@jsquash/avif: Multi-threaded, industrial-grade AVIF encoding algorithms compiled to WASM.&lt;/li&gt;
&lt;li&gt;fflate: High-speed, memory-efficient binary ZIP compression.&lt;/li&gt;
&lt;li&gt;StreamSaver: Low-overhead client-side streaming downloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance Benchmarks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Testing the pipeline on an 8-core machine yielded impressive results, proving that browser storage can compete with native desktop tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 high-res images (10MB each): ~45 seconds&lt;/li&gt;
&lt;li&gt;500 mobile assets (2MB each): ~2 minutes&lt;/li&gt;
&lt;li&gt;Max RAM Usage: Stays under 200MB at all times&lt;/li&gt;
&lt;li&gt;Disk Usage: Streamed directly to OPFS, zero memory leak&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Check it out!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project is completely free, open-source, and has no trackers, cookies, or ads.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sapianyi.github.io/PixelForge-Mass/" rel="noopener noreferrer"&gt;Live App: https://sapianyi.github.io/PixelForge-Mass/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sapianyi/PixelForge-Mass" rel="noopener noreferrer"&gt;Source Code: https://github.com/Sapianyi/PixelForge-Mass&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would love to hear your thoughts on this architecture! How are you handling heavy asset manipulation on the client side? Let's discuss in the comments!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
