<?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: Digitalofen</title>
    <description>The latest articles on DEV Community by Digitalofen (@digitalofen).</description>
    <link>https://dev.to/digitalofen</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%2F3781673%2F3f5d428c-c843-4101-b192-310b005825c7.png</url>
      <title>DEV Community: Digitalofen</title>
      <link>https://dev.to/digitalofen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/digitalofen"/>
    <language>en</language>
    <item>
      <title>I Tried Running File Conversion Fully in the Browser (WASM, LibreOffice, FFmpeg)</title>
      <dc:creator>Digitalofen</dc:creator>
      <pubDate>Thu, 19 Feb 2026 23:35:16 +0000</pubDate>
      <link>https://dev.to/digitalofen/i-tried-running-file-conversion-fully-in-the-browser-wasm-libreoffice-ffmpeg-57mh</link>
      <guid>https://dev.to/digitalofen/i-tried-running-file-conversion-fully-in-the-browser-wasm-libreoffice-ffmpeg-57mh</guid>
      <description>&lt;h2&gt;
  
  
  Intro – The Problem
&lt;/h2&gt;

&lt;p&gt;Most online file converters work the same way: you upload your file, wait, then download the result. That raises privacy concerns and doubles transfer time for large files (upload + download = double the time).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personal context:&lt;/strong&gt; I'm an audio producer and needed to convert some legacy audio formats for a project. I tried 5 different converter sites but none of them supported the formats I needed - or hit me with endless pop-ups and CAPTCHA walls. The whole experience felt quite sketchy and so I thought: there has to be a better way. Turns out: there is – but it's not as simple as I thought.&lt;/p&gt;

&lt;p&gt;I wondered: &lt;strong&gt;how much of this can realistically run in the browser?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turns out: more than I expected, but with hard limits nobody talks about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LibreOffice WASM Dream (That Didn't Work)
&lt;/h2&gt;

&lt;p&gt;My first idea: compile LibreOffice headless to WebAssembly. It's the Swiss Army knife for document conversion (DOCX, ODT, PDF, PPTX...). If I could get it running in the browser, I'd be done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reality check:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum binary size: ~150MB (even stripped)&lt;/li&gt;
&lt;li&gt;Startup time: 10-15 seconds just to initialize&lt;/li&gt;
&lt;li&gt;Memory usage: 200-300MB for a single DOCX→PDF conversion&lt;/li&gt;
&lt;li&gt;Browser memory ceiling: ~500MB before crashes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For converting one file? Not realistic today in a production browser environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Works in the Browser
&lt;/h2&gt;

&lt;p&gt;Instead, I went format-by-format. Here's what I got working:&lt;/p&gt;

&lt;h3&gt;
  
  
  Video/Audio
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg.wasm&lt;/strong&gt; (~20MB WASM binary)&lt;/li&gt;
&lt;li&gt;Performance: ~10-20% of native FFmpeg&lt;/li&gt;
&lt;li&gt;Example: MP3 conversion takes ~10s (native: 1s)&lt;/li&gt;
&lt;li&gt;Trade-off: Slower, but files never leave device&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Images
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sharp.wasm&lt;/strong&gt; + &lt;strong&gt;image-js&lt;/strong&gt; + &lt;strong&gt;canvg&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Performance: Image resize ~2s (native: 0.2s)&lt;/li&gt;
&lt;li&gt;Handles: PNG, JPEG, WebP, SVG→PNG, GIF&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  PDFs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pdf-lib&lt;/strong&gt; (creation/manipulation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pdf.js&lt;/strong&gt; (rendering/text extraction)&lt;/li&gt;
&lt;li&gt;Works great for simple operations (merge, split, text extraction)&lt;/li&gt;
&lt;li&gt;Fails on complex PDFs with embedded fonts/forms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Spreadsheets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SheetJS&lt;/strong&gt; (xlsx.full.min.js)&lt;/li&gt;
&lt;li&gt;Parses/creates Excel, CSV, ODS in browser&lt;/li&gt;
&lt;li&gt;Limitations: Large files (&amp;gt;10MB) freeze UI&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Archives
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSZip&lt;/strong&gt; + &lt;strong&gt;pako&lt;/strong&gt; + &lt;strong&gt;tar.js&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;ZIP/GZIP/TAR creation/extraction&lt;/li&gt;
&lt;li&gt;Fast enough for most use cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Optimization strategy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lazy load WASM modules on demand&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadFFmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ffmpegLoaded&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ffmpeg/ffmpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ffmpegLoaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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="c1"&gt;// Total WASM payload: ~30MB&lt;/span&gt;
&lt;span class="c1"&gt;// Initial bundle: 150KB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Performance Win I didn't expect
&lt;/h2&gt;

&lt;p&gt;Here's a trick that saved me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; MP4→MOV conversion was timing out after &amp;gt;10 minutes. Full re-encoding with FFmpeg.wasm is just too slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; Container remux instead of re-encode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Check if codecs are compatible&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compatibleContainers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mov&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mkv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mov&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mkv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mkv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mov&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;areCompatibleContainers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Stream copy instead of re-encode&lt;/span&gt;
  &lt;span class="nx"&gt;ffmpegOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Result: &amp;lt;10s instead of 10min (100× speedup)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works because MP4 and MOV use the same codecs (H.264, AAC) – you're just changing the container wrapper.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hard Parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Browser Memory Limits
&lt;/h3&gt;

&lt;p&gt;Safari: ~500MB, then crash&lt;br&gt;&lt;br&gt;
Chrome: ~1GB, then slow-down&lt;br&gt;&lt;br&gt;
Firefox: ~800MB&lt;/p&gt;

&lt;p&gt;Large video files? Forget it. One hits the ceiling fast.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Safari vs Chromium Hell
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Chrome: &lt;code&gt;SharedArrayBuffer&lt;/code&gt; works (multi-threading possible)&lt;/li&gt;
&lt;li&gt;Safari: &lt;code&gt;SharedArrayBuffer&lt;/code&gt; requires COOP/COEP headers (breaks CDNs)&lt;/li&gt;
&lt;li&gt;Solution: Detect and fallback to single-threaded mode
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canUseMultithreading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;SharedArrayBuffer&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOriginIsolated&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&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;threadCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canUseMultithreading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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;h3&gt;
  
  
  3. Large File Streaming
&lt;/h3&gt;

&lt;p&gt;You can't load a 500MB video into memory. Browsers will kill the tab.&lt;/p&gt;

&lt;p&gt;I tried: File streaming via &lt;code&gt;ReadableStream&lt;/code&gt;&lt;br&gt;&lt;br&gt;
The reality: FFmpeg.wasm doesn't support streaming input yet (2025)&lt;/p&gt;
&lt;h3&gt;
  
  
  4. UI Freezing
&lt;/h3&gt;

&lt;p&gt;Even with Web Workers, WASM blocks the main thread during initialization.&lt;/p&gt;

&lt;p&gt;The solution: Show loading screen + delay 100ms to let UI render first.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Hybrid Model (What I Actually Ended Up With. For Now.)
&lt;/h2&gt;

&lt;p&gt;After weeks of experiments, here's what works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture split:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shouldUseServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Browser-side (90% of conversions)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;fileSize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Server-side (10% of conversions)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;docx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&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;// LibreOffice&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;complex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;// Poppler&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;// Too large&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;&lt;strong&gt;Server-side stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LibreOffice (headless) for Office docs&lt;/li&gt;
&lt;li&gt;Pandoc for Markdown/LaTeX/EPUB&lt;/li&gt;
&lt;li&gt;Native FFmpeg for large videos&lt;/li&gt;
&lt;li&gt;Poppler (pdftoppm, pdftotext) for complex PDFs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClamAV malware scanning&lt;/strong&gt; on all uploads (auto-reject infected files)&lt;/li&gt;
&lt;li&gt;Auto-delete immediately / after 5 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why hybrid?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pure client-side = unrealistic for complex formats&lt;/li&gt;
&lt;li&gt;Pure server-side = privacy nightmare + infrastructure costs&lt;/li&gt;
&lt;li&gt;Hybrid = best UX + privacy trade-off&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Real Trade-offs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Client-Side Pros:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Privacy (files never leave device)
&lt;/li&gt;
&lt;li&gt;Zero infrastructure costs
&lt;/li&gt;
&lt;li&gt;Instant conversion (no upload wait)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Client-Side Cons:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;10-20% performance of native
&lt;/li&gt;
&lt;li&gt;Memory limits (~50MB practical ceiling)
&lt;/li&gt;
&lt;li&gt;Browser compatibility hell
&lt;/li&gt;
&lt;li&gt;Some formats impossible (Office docs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Server-Side Pros:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Full performance (native tools)
&lt;/li&gt;
&lt;li&gt;Unlimited file size
&lt;/li&gt;
&lt;li&gt;Complex formats supported
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Server-Side Cons:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Privacy concerns (uploads)
&lt;/li&gt;
&lt;li&gt;Infrastructure costs
&lt;/li&gt;
&lt;li&gt;Upload/download overhead&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Client&lt;br&gt;
 ├── FFmpeg.wasm&lt;br&gt;
 ├── Image tooling&lt;br&gt;
 ├── PDF manipulation&lt;br&gt;
 └── SheetJS&lt;br&gt;
        ↓ (fallback)&lt;br&gt;
Server&lt;br&gt;
 ├── LibreOffice&lt;br&gt;
 ├── Pandoc&lt;br&gt;
 ├── Native FFmpeg&lt;br&gt;
 └── Poppler&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pure browser-side is possible for ~90% of conversions&lt;/strong&gt; (images, audio, simple video)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LibreOffice WASM is still unrealistic / unstable&lt;/strong&gt; in 2026 (too large, too slow, too browser/platform-dependent)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid architecture is the pragmatic solution&lt;/strong&gt; (browser-first, server fallback)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container remux &amp;gt; re-encode&lt;/strong&gt; when possible (100× speedup)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory limits are the real bottleneck&lt;/strong&gt;, not CPU&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building something similar: start with WASM, fall back to server only when necessary, and be honest with users about what runs where.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Based on these experiments, I ended up packaging this hybrid approach into a small project to test it in production.&lt;/p&gt;

&lt;p&gt;Tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: Next.js (static), FFmpeg.wasm, Sharp.wasm, pdf-lib, SheetJS&lt;/li&gt;
&lt;li&gt;Backend: Node/Express, LibreOffice, Pandoc, ClamAV, Redis&lt;/li&gt;
&lt;li&gt;Hosting: Cloudflare CDN (frontend), EU server (backend)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;→ If you're curious: anythingconverter.com&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Questions?&lt;/strong&gt; I'm especially curious if anyone has cracked LibreOffice WASM or found better ways to handle large files in the browser.&lt;/p&gt;

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