<?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: Sora Labs</title>
    <description>The latest articles on DEV Community by Sora Labs (@soralabs).</description>
    <link>https://dev.to/soralabs</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%2F4094556%2F52a22491-8473-4b56-b91a-36a9a8039fab.png</url>
      <title>DEV Community: Sora Labs</title>
      <link>https://dev.to/soralabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/soralabs"/>
    <language>en</language>
    <item>
      <title>A blob URL needs an owner, not just a cleanup call</title>
      <dc:creator>Sora Labs</dc:creator>
      <pubDate>Sun, 04 Oct 2026 11:45:18 +0000</pubDate>
      <link>https://dev.to/soralabs/a-blob-url-needs-an-owner-not-just-a-cleanup-call-2bd4</link>
      <guid>https://dev.to/soralabs/a-blob-url-needs-an-owner-not-just-a-cleanup-call-2bd4</guid>
      <description>&lt;p&gt;Disclosure: This article was generated with AI from a source-code review and browser API documentation. Its API claims were checked against the linked MDN references, and the example's lifecycle behavior was tested in a mocked DOM. It is not a report of measured production results or real-browser compatibility testing.&lt;/p&gt;

&lt;p&gt;A file-preview widget can look complete when an image appears and its download link works. There is another question worth answering: what ends the lifetime of the URL connecting those elements to the file?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;URL.createObjectURL(blob)&lt;/code&gt; creates a browser-managed URL referring to a Blob or File. It does not create a hosted download. Treat that URL as a resource the widget owns, rather than an arbitrary string to leave behind. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static" rel="noopener noreferrer"&gt;MDN: createObjectURL&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Define when the UI stops needing it
&lt;/h2&gt;

&lt;p&gt;For a preview with a download link, reasonable boundaries are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user replaces the selected file.&lt;/li&gt;
&lt;li&gt;The user resets the widget.&lt;/li&gt;
&lt;li&gt;The component is disposed by the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The image finishing its initial load is not necessarily that boundary: the user might still need the URL for a download or an image interaction. MDN's image example cautions against revoking the URL too early. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_object_urls" rel="noopener noreferrer"&gt;MDN: using files&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the preview and download one shared owner
&lt;/h2&gt;

&lt;p&gt;Here is a small synchronous example. It previews a selected image and offers the original file for download; it performs no conversion or compression.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"file-input"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;accept=&lt;/span&gt;&lt;span class="s"&gt;"image/*"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"preview"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Selected image preview"&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"download"&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Download original&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"reset"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reset&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;fileInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#file-input&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;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#preview&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;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#download&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;resetButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ownedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clearPreview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&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="nx"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&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="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;download&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;ownedUrl&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ownedUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ownedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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="nx"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearPreview&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;selectedFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&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;selectedFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;ownedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ownedUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ownedUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="nx"&gt;resetButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearPreview&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both elements share a URL, so cleanup waits until neither should use it. Resetting twice is safe because the owner is cleared after revocation. An application that removes this component should also remove its event listeners and call &lt;code&gt;clearPreview()&lt;/code&gt; during disposal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;accept="image/*"&lt;/code&gt; is a picker hint, not file validation. A complete widget should handle unsupported or corrupt images, errors and accessibility requirements. This deliberately small example does not solve those concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not mistake revocation for secure deletion
&lt;/h2&gt;

&lt;p&gt;Revocation releases the object URL's association with its backing object. It does not prove that all file-related memory has immediately disappeared or that bytes have been securely erased. Other references or decoded image data can have separate lifetimes. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static" rel="noopener noreferrer"&gt;MDN: revokeObjectURL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If processing is asynchronous, resource cleanup is also not cancellation. An older task may finish after a reset. Check that it is still the current task before attaching a newly created result URL. This example avoids that separate problem by doing no asynchronous processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the lifecycle, not just the first render
&lt;/h2&gt;

&lt;p&gt;Try selecting image A, replacing it with image B, resetting twice, selecting again, and downloading while the preview remains visible. During component teardown, verify that its URL and listeners are released. Instrument create/revoke calls in a test adapter if you need deterministic accounting; do not infer success merely from an immediate drop in reported browser memory.&lt;/p&gt;

&lt;p&gt;The useful design question is simple: which part of the interface owns this resource, and which user action ends that ownership?&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
    <item>
      <title>Why Browser-Based PDF Tools Can Be More Private Than Cloud PDF Converters</title>
      <dc:creator>Sora Labs</dc:creator>
      <pubDate>Tue, 01 Sep 2026 16:33:19 +0000</pubDate>
      <link>https://dev.to/soralabs/why-browser-based-pdf-tools-can-be-more-private-than-cloud-pdf-converters-24n3</link>
      <guid>https://dev.to/soralabs/why-browser-based-pdf-tools-can-be-more-private-than-cloud-pdf-converters-24n3</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvm4ugrx4lmv1j9ca42hx.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvm4ugrx4lmv1j9ca42hx.png" alt=" " width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PDF tools are everywhere.&lt;/p&gt;

&lt;p&gt;Compress a PDF. Merge two files. Convert an image. Remove metadata. Sign a document.&lt;/p&gt;

&lt;p&gt;Most of us use these tools without thinking much about what happens after clicking Upload.&lt;/p&gt;

&lt;p&gt;But there is an important architectural difference between two tools that may look identical from the outside:&lt;/p&gt;

&lt;p&gt;One uploads your document to a remote server.&lt;/p&gt;

&lt;p&gt;The other processes it directly inside your browser.&lt;/p&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;The traditional model: upload, process, download&lt;/p&gt;

&lt;p&gt;A typical online document tool works roughly like this:&lt;/p&gt;

&lt;p&gt;You select a file.&lt;br&gt;
Your browser uploads it to the provider's server.&lt;br&gt;
The server processes the document.&lt;br&gt;
You download the result.&lt;br&gt;
The provider hopefully deletes the uploaded copy.&lt;/p&gt;

&lt;p&gt;This architecture isn't automatically unsafe. Reputable companies can use encryption, access controls, short retention periods and strong infrastructure.&lt;/p&gt;

&lt;p&gt;But it introduces something that doesn't need to exist for many document-processing tasks:&lt;/p&gt;

&lt;p&gt;your file leaves your device.&lt;/p&gt;

&lt;p&gt;That creates additional questions.&lt;/p&gt;

&lt;p&gt;How long is the file stored?&lt;/p&gt;

&lt;p&gt;Which server processes it?&lt;/p&gt;

&lt;p&gt;Who can access the infrastructure?&lt;/p&gt;

&lt;p&gt;Are backups created?&lt;/p&gt;

&lt;p&gt;Are uploaded documents logged?&lt;/p&gt;

&lt;p&gt;What happens if the service is compromised?&lt;/p&gt;

&lt;p&gt;For ordinary photos, these questions might not matter much.&lt;/p&gt;

&lt;p&gt;For tax documents, contracts, identification documents, invoices, medical files or internal company PDFs, they can matter considerably.&lt;/p&gt;

&lt;p&gt;Modern browsers are surprisingly capable&lt;/p&gt;

&lt;p&gt;Browsers are no longer simple document viewers.&lt;/p&gt;

&lt;p&gt;Modern JavaScript, WebAssembly, Web Workers and browser APIs allow surprisingly complex operations to happen locally.&lt;/p&gt;

&lt;p&gt;A web application can potentially:&lt;/p&gt;

&lt;p&gt;merge PDFs&lt;br&gt;
split PDFs&lt;br&gt;
rotate pages&lt;br&gt;
manipulate images&lt;br&gt;
resize images&lt;br&gt;
compress certain files&lt;br&gt;
remove metadata&lt;br&gt;
generate documents&lt;br&gt;
perform OCR&lt;br&gt;
process media&lt;/p&gt;

&lt;p&gt;without uploading the original document to a remote server.&lt;/p&gt;

&lt;p&gt;The website still needs to download application code, of course.&lt;/p&gt;

&lt;p&gt;But once that code is running, the actual user file can remain on the user's machine.&lt;/p&gt;

&lt;p&gt;The architecture becomes:&lt;/p&gt;

&lt;p&gt;File → Browser → Processing → Result&lt;/p&gt;

&lt;p&gt;instead of:&lt;/p&gt;

&lt;p&gt;File → Internet → Server → Processing → Internet → Result&lt;/p&gt;

&lt;p&gt;That is a meaningful reduction in exposure.&lt;/p&gt;

&lt;p&gt;Local processing isn't automatically private&lt;/p&gt;

&lt;p&gt;There is an important caveat.&lt;/p&gt;

&lt;p&gt;A website claiming to run "in your browser" doesn't automatically mean everything stays local.&lt;/p&gt;

&lt;p&gt;The application could still:&lt;/p&gt;

&lt;p&gt;upload files through another API&lt;br&gt;
send extracted text to analytics services&lt;br&gt;
transmit metadata&lt;br&gt;
use server processing for specific operations&lt;br&gt;
make third-party network requests&lt;/p&gt;

&lt;p&gt;So the privacy claim should be technically verifiable.&lt;/p&gt;

&lt;p&gt;Developers building local-first tools should make the architecture explicit and minimize unnecessary network access.&lt;/p&gt;

&lt;p&gt;Browser developer tools can also help technically inclined users inspect network traffic while a document is being processed.&lt;/p&gt;

&lt;p&gt;Why I started building around this model&lt;/p&gt;

&lt;p&gt;While working with online document utilities, I kept coming back to a simple question:&lt;/p&gt;

&lt;p&gt;Why upload a private file when the user's own computer can perform the operation?&lt;/p&gt;

&lt;p&gt;That became one of the principles behind &lt;a href="https://sorafiles.com/" rel="noopener noreferrer"&gt;SoraFiles&lt;/a&gt;, a privacy-first collection of PDF, image and document tools I'm building around browser-local processing wherever practical.&lt;/p&gt;

&lt;p&gt;The goal isn't merely to put traditional server-side utilities into another interface.&lt;/p&gt;

&lt;p&gt;The more interesting challenge is determining how much document processing can realistically be moved to the client while keeping the experience fast and usable.&lt;/p&gt;

&lt;p&gt;That introduces engineering tradeoffs of its own.&lt;/p&gt;

&lt;p&gt;Local processing has disadvantages too&lt;/p&gt;

&lt;p&gt;Client-side processing is not a magical solution.&lt;/p&gt;

&lt;p&gt;Device performance varies&lt;/p&gt;

&lt;p&gt;A powerful desktop computer and an inexpensive smartphone have dramatically different processing capabilities.&lt;/p&gt;

&lt;p&gt;Heavy operations can consume:&lt;/p&gt;

&lt;p&gt;CPU&lt;br&gt;
RAM&lt;br&gt;
battery&lt;br&gt;
browser storage&lt;/p&gt;

&lt;p&gt;Developers therefore need sensible file limits and graceful failure states.&lt;/p&gt;

&lt;p&gt;Large engines increase download size&lt;/p&gt;

&lt;p&gt;Some advanced document-processing libraries and machine-learning models can be very large.&lt;/p&gt;

&lt;p&gt;Downloading hundreds of megabytes just to perform a simple operation would create a terrible user experience.&lt;/p&gt;

&lt;p&gt;Lazy loading and caching can help, but there is always a tradeoff.&lt;/p&gt;

&lt;p&gt;Some operations genuinely belong on servers&lt;/p&gt;

&lt;p&gt;Certain jobs may require infrastructure that isn't realistic inside a browser.&lt;/p&gt;

&lt;p&gt;Large-scale OCR, complex document reconstruction, very large files and computationally expensive AI workflows are examples.&lt;/p&gt;

&lt;p&gt;The correct architectural goal therefore shouldn't be:&lt;/p&gt;

&lt;p&gt;"Servers are bad."&lt;/p&gt;

&lt;p&gt;It should be:&lt;/p&gt;

&lt;p&gt;"Don't send user data to a server unless the operation actually requires it."&lt;/p&gt;

&lt;p&gt;Local-first can improve performance too&lt;/p&gt;

&lt;p&gt;Privacy isn't the only benefit.&lt;/p&gt;

&lt;p&gt;Removing the upload/download cycle can make some tools noticeably faster.&lt;/p&gt;

&lt;p&gt;Consider a 100 MB document.&lt;/p&gt;

&lt;p&gt;A server-based application may need to:&lt;/p&gt;

&lt;p&gt;upload 100 MB&lt;br&gt;
process it&lt;br&gt;
download the result&lt;/p&gt;

&lt;p&gt;On a slow connection, file transfer can take considerably longer than the actual computation.&lt;/p&gt;

&lt;p&gt;With local processing, the file is already on the device.&lt;/p&gt;

&lt;p&gt;This also makes some tools more resilient on unreliable internet connections.&lt;/p&gt;

&lt;p&gt;It changes the economics of running the service&lt;/p&gt;

&lt;p&gt;There is another advantage developers don't always discuss.&lt;/p&gt;

&lt;p&gt;Server-side document processing costs money.&lt;/p&gt;

&lt;p&gt;Large files consume:&lt;/p&gt;

&lt;p&gt;bandwidth&lt;br&gt;
temporary storage&lt;br&gt;
compute&lt;br&gt;
memory&lt;br&gt;
queue capacity&lt;/p&gt;

&lt;p&gt;As usage grows, infrastructure expenses grow with it.&lt;/p&gt;

&lt;p&gt;When users perform appropriate workloads on their own devices, much of that variable infrastructure cost disappears.&lt;/p&gt;

&lt;p&gt;That can make it easier to provide useful tools for free without aggressively monetizing users.&lt;/p&gt;

&lt;p&gt;Privacy can be an architectural decision&lt;/p&gt;

&lt;p&gt;Privacy policies are useful.&lt;/p&gt;

&lt;p&gt;Encryption is important.&lt;/p&gt;

&lt;p&gt;Retention policies matter.&lt;/p&gt;

&lt;p&gt;But one of the strongest privacy protections is simply not collecting the data in the first place.&lt;/p&gt;

&lt;p&gt;If an operation can safely happen on the user's device, there is often little reason for the application developer to receive the original file at all.&lt;/p&gt;

&lt;p&gt;That's why I think we'll see more web applications adopt local-first or hybrid architectures over the next few years.&lt;/p&gt;

&lt;p&gt;The browser has quietly become a capable application runtime.&lt;/p&gt;

&lt;p&gt;We should take advantage of it.&lt;/p&gt;

&lt;p&gt;I'm experimenting with this approach while building SoraFiles, and I'd be interested to hear from other developers working with WebAssembly, local-first applications or browser-based document processing.&lt;/p&gt;

&lt;p&gt;What workloads have you successfully moved from the server to the browser?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>7 Engineering Lessons From Building File Processing Directly in the Browser</title>
      <dc:creator>Sora Labs</dc:creator>
      <pubDate>Tue, 25 Aug 2026 17:31:57 +0000</pubDate>
      <link>https://dev.to/soralabs/7-engineering-lessons-from-building-file-processing-directly-in-the-browser-571d</link>
      <guid>https://dev.to/soralabs/7-engineering-lessons-from-building-file-processing-directly-in-the-browser-571d</guid>
      <description>&lt;p&gt;I've been building SoraFiles, a privacy-first web app for working with PDFs and images directly in the browser.&lt;/p&gt;

&lt;p&gt;The interesting part has not been creating another upload form.&lt;/p&gt;

&lt;p&gt;It has been removing the upload step entirely for supported workflows.&lt;/p&gt;

&lt;p&gt;The basic architecture is:&lt;/p&gt;

&lt;p&gt;User file&lt;br&gt;
-&amp;gt; Browser File API&lt;br&gt;
-&amp;gt; Local JavaScript / WebAssembly processing&lt;br&gt;
-&amp;gt; Result&lt;br&gt;
-&amp;gt; Download&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;Browser&lt;br&gt;
-&amp;gt; Upload&lt;br&gt;
-&amp;gt; Processing server&lt;br&gt;
-&amp;gt; Temporary storage&lt;br&gt;
-&amp;gt; Download&lt;/p&gt;

&lt;p&gt;That sounds simple until you try to make it reliable on phones, Safari, large PDFs, corrupt files, repeated user actions, and limited browser memory.&lt;/p&gt;

&lt;p&gt;Here are seven engineering lessons that have mattered most.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  1. TREAT EVERY PROCESSING OPERATION AS A JOB
&lt;/h1&gt;

&lt;p&gt;The simplest implementation is also the one most likely to create stale-state bugs.&lt;/p&gt;

&lt;p&gt;A user clicks Process.&lt;/p&gt;

&lt;p&gt;The job starts.&lt;/p&gt;

&lt;p&gt;Before it finishes, they remove the file and select another one.&lt;/p&gt;

&lt;p&gt;If the first operation is still allowed to update the interface, the old result can suddenly appear inside the new workflow.&lt;/p&gt;

&lt;p&gt;That is confusing and dangerous.&lt;/p&gt;

&lt;p&gt;A better model is to treat every processing operation as a job with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a unique identity&lt;/li&gt;
&lt;li&gt;a state&lt;/li&gt;
&lt;li&gt;cancellation&lt;/li&gt;
&lt;li&gt;ownership of its resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lifecycle can be thought of as:&lt;/p&gt;

&lt;p&gt;Idle&lt;br&gt;
-&amp;gt; Ready&lt;br&gt;
-&amp;gt; Processing&lt;br&gt;
-&amp;gt; Success / Error / Cancelled&lt;/p&gt;

&lt;p&gt;Before any asynchronous task updates the UI, the application should verify that it still belongs to the current active job.&lt;/p&gt;

&lt;p&gt;That one idea prevents a surprising number of bugs.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  2. WEB WORKERS HELP, BUT OWNERSHIP STILL MATTERS
&lt;/h1&gt;

&lt;p&gt;PDF rendering, OCR, compression, and image conversion can become expensive quickly.&lt;/p&gt;

&lt;p&gt;Moving heavy work into Web Workers helps keep the interface responsive.&lt;/p&gt;

&lt;p&gt;But a Worker does not magically solve memory problems.&lt;/p&gt;

&lt;p&gt;Large buffers may still need to move between the main thread and the Worker.&lt;/p&gt;

&lt;p&gt;So the important questions become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who owns this buffer?&lt;/li&gt;
&lt;li&gt;Was it copied or transferred?&lt;/li&gt;
&lt;li&gt;Can the main thread still use it?&lt;/li&gt;
&lt;li&gt;What happens when the user cancels?&lt;/li&gt;
&lt;li&gt;Who cleans it up?&lt;/li&gt;
&lt;li&gt;Can the Worker be terminated safely?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local processing becomes much easier to reason about when data ownership is explicit.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  3. CLEANUP IS PART OF THE ALGORITHM
&lt;/h1&gt;

&lt;p&gt;A browser-local file application can create a lot of temporary resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;canvases&lt;/li&gt;
&lt;li&gt;image bitmaps&lt;/li&gt;
&lt;li&gt;PDF rendering tasks&lt;/li&gt;
&lt;li&gt;object URLs&lt;/li&gt;
&lt;li&gt;Blobs&lt;/li&gt;
&lt;li&gt;ArrayBuffers&lt;/li&gt;
&lt;li&gt;workers&lt;/li&gt;
&lt;li&gt;event listeners&lt;/li&gt;
&lt;li&gt;temporary decoded documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you create them and never clean them up, the browser may keep enough memory alive to make the app unstable on mobile.&lt;/p&gt;

&lt;p&gt;I now think of cleanup as part of the actual processing pipeline:&lt;/p&gt;

&lt;p&gt;Validate&lt;br&gt;
-&amp;gt; Decode&lt;br&gt;
-&amp;gt; Process&lt;br&gt;
-&amp;gt; Encode&lt;br&gt;
-&amp;gt; Expose result&lt;br&gt;
-&amp;gt; Cleanup&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;p&gt;Process&lt;br&gt;
-&amp;gt; Hope garbage collection eventually fixes everything&lt;/p&gt;

&lt;p&gt;This matters especially on iPhones and lower-memory devices.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  4. A 20 MB FILE DOES NOT MEAN 20 MB OF MEMORY
&lt;/h1&gt;

&lt;p&gt;Compressed file size and in-memory size are very different things.&lt;/p&gt;

&lt;p&gt;A 20 MB PDF may temporarily involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the original ArrayBuffer&lt;/li&gt;
&lt;li&gt;decoded page data&lt;/li&gt;
&lt;li&gt;several canvases&lt;/li&gt;
&lt;li&gt;preview images&lt;/li&gt;
&lt;li&gt;intermediate output&lt;/li&gt;
&lt;li&gt;the final result Blob&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Images can expand dramatically when decoded into raw pixels.&lt;/p&gt;

&lt;p&gt;So large-file support is not just about removing an arbitrary upload limit.&lt;/p&gt;

&lt;p&gt;It requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bounded concurrency&lt;/li&gt;
&lt;li&gt;sequential processing where possible&lt;/li&gt;
&lt;li&gt;avoiding unnecessary buffer copies&lt;/li&gt;
&lt;li&gt;releasing resources quickly&lt;/li&gt;
&lt;li&gt;lower-resolution previews&lt;/li&gt;
&lt;li&gt;cancellation&lt;/li&gt;
&lt;li&gt;capability-aware limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Desktop browsers can hide inefficient architecture.&lt;/p&gt;

&lt;p&gt;Phones expose it.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  5. PREVIEW RENDERING CAN BECOME A PERFORMANCE BUG
&lt;/h1&gt;

&lt;p&gt;A preview looks like a UI problem.&lt;/p&gt;

&lt;p&gt;It is also a memory problem.&lt;/p&gt;

&lt;p&gt;Suppose a PDF page is displayed inside a 320-pixel-wide card.&lt;/p&gt;

&lt;p&gt;If you render that page internally at several thousand pixels and simply shrink it with CSS, the preview may look correct while still consuming far more memory than necessary.&lt;/p&gt;

&lt;p&gt;A better approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Determine the actual visible preview size.&lt;/li&gt;
&lt;li&gt;Account for device pixel ratio.&lt;/li&gt;
&lt;li&gt;Render close to the resolution the UI genuinely needs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The browser is both your renderer and your processing machine.&lt;/p&gt;

&lt;p&gt;Waste resources on the preview and you leave fewer resources for the actual file operation.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  6. DEFINE WHAT ANALYTICS MUST NEVER RECEIVE
&lt;/h1&gt;

&lt;p&gt;Analytics usually starts with:&lt;/p&gt;

&lt;p&gt;"What should we track?"&lt;/p&gt;

&lt;p&gt;For software handling private documents, I think the first question should be:&lt;/p&gt;

&lt;p&gt;"What should never reach analytics?"&lt;/p&gt;

&lt;p&gt;For SoraFiles, file-derived information should remain outside the analytics layer.&lt;/p&gt;

&lt;p&gt;That includes things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;filenames&lt;/li&gt;
&lt;li&gt;document text&lt;/li&gt;
&lt;li&gt;OCR output&lt;/li&gt;
&lt;li&gt;metadata values&lt;/li&gt;
&lt;li&gt;passwords&lt;/li&gt;
&lt;li&gt;signatures&lt;/li&gt;
&lt;li&gt;image pixels&lt;/li&gt;
&lt;li&gt;generated file contents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can still learn whether the product works.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Tool opened&lt;br&gt;
-&amp;gt; Processing started&lt;br&gt;
-&amp;gt; Processing succeeded&lt;/p&gt;

&lt;p&gt;That tells you whether users can complete a workflow.&lt;/p&gt;

&lt;p&gt;You usually do not need to know what was inside their document.&lt;/p&gt;

&lt;p&gt;The rule I like is:&lt;/p&gt;

&lt;p&gt;Analytics should answer "Did the workflow work?" without answering "What was inside the user's file?"&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  7. HONEST LIMITATIONS BUILD MORE TRUST THAN PERFECT-SOUNDING CLAIMS
&lt;/h1&gt;

&lt;p&gt;Some file operations are relatively straightforward.&lt;/p&gt;

&lt;p&gt;Others are not.&lt;/p&gt;

&lt;p&gt;Examples of difficult workflows include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF to Word&lt;/li&gt;
&lt;li&gt;Word to PDF&lt;/li&gt;
&lt;li&gt;OCR&lt;/li&gt;
&lt;li&gt;PDF repair&lt;/li&gt;
&lt;li&gt;complex spreadsheets&lt;/li&gt;
&lt;li&gt;unusual fonts&lt;/li&gt;
&lt;li&gt;forms&lt;/li&gt;
&lt;li&gt;embedded objects&lt;/li&gt;
&lt;li&gt;complicated page layouts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser libraries are powerful, but they do not perfectly reproduce every edge case.&lt;/p&gt;

&lt;p&gt;If a conversion may simplify a complex layout, tell the user before processing.&lt;/p&gt;

&lt;p&gt;If transparency must be flattened, explain it.&lt;/p&gt;

&lt;p&gt;If a very large file may exceed the browser's practical memory limit, say so.&lt;/p&gt;

&lt;p&gt;Users can understand limitations.&lt;/p&gt;

&lt;p&gt;What destroys trust is discovering them only after downloading the result.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  WHY LOCAL PROCESSING IS INTERESTING
&lt;/h1&gt;

&lt;p&gt;Server-side processing still has real advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable compute&lt;/li&gt;
&lt;li&gt;larger memory pools&lt;/li&gt;
&lt;li&gt;powerful native libraries&lt;/li&gt;
&lt;li&gt;easier handling of huge files&lt;/li&gt;
&lt;li&gt;consistent execution environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local processing has different advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;files can stay on the user's device&lt;/li&gt;
&lt;li&gt;no upload wait&lt;/li&gt;
&lt;li&gt;reduced server-side exposure&lt;/li&gt;
&lt;li&gt;some workflows can continue offline&lt;/li&gt;
&lt;li&gt;less processing infrastructure is required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I do not think the right question is:&lt;/p&gt;

&lt;p&gt;"Should everything run locally?"&lt;/p&gt;

&lt;p&gt;A better question is:&lt;/p&gt;

&lt;p&gt;"Does this operation genuinely require the user's file to leave their device?"&lt;/p&gt;

&lt;p&gt;For many routine PDF and image operations, modern browsers increasingly make the answer:&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  OPEN SOURCE AND INSPECTABILITY
&lt;/h1&gt;

&lt;p&gt;SoraFiles is open source under AGPL-3.0.&lt;/p&gt;

&lt;p&gt;Open source does not automatically make software secure.&lt;/p&gt;

&lt;p&gt;It does not replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;dependency review&lt;/li&gt;
&lt;li&gt;secure implementation&lt;/li&gt;
&lt;li&gt;privacy engineering&lt;/li&gt;
&lt;li&gt;vulnerability management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it makes architectural claims easier to inspect.&lt;/p&gt;

&lt;p&gt;People can look at the code and see how the processing path works.&lt;/p&gt;

&lt;p&gt;For privacy-focused software, that inspectability matters.&lt;/p&gt;

&lt;p&gt;============================================================&lt;/p&gt;

&lt;h1&gt;
  
  
  CLOSING THOUGHT
&lt;/h1&gt;

&lt;p&gt;Building file processing directly inside the browser changed the way I think about privacy engineering.&lt;/p&gt;

&lt;p&gt;The strongest privacy improvement was not another paragraph in a privacy policy.&lt;/p&gt;

&lt;p&gt;It was removing unnecessary data movement from the architecture.&lt;/p&gt;

&lt;p&gt;Once that decision is made, privacy becomes an engineering problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory management&lt;/li&gt;
&lt;li&gt;workers&lt;/li&gt;
&lt;li&gt;cancellation&lt;/li&gt;
&lt;li&gt;state machines&lt;/li&gt;
&lt;li&gt;browser compatibility&lt;/li&gt;
&lt;li&gt;output validation&lt;/li&gt;
&lt;li&gt;honest limitations&lt;/li&gt;
&lt;li&gt;carefully separated analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That work is less visible than putting a privacy badge on a landing page.&lt;/p&gt;

&lt;p&gt;It is also what makes the privacy claim meaningful.&lt;/p&gt;

&lt;p&gt;If the user's device is already capable of doing the work, uploading their private file should not automatically be the default.&lt;/p&gt;

&lt;p&gt;SoraFiles:&lt;br&gt;
&lt;a href="https://sorafiles.com" rel="noopener noreferrer"&gt;https://sorafiles.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source code:&lt;br&gt;
&lt;a href="https://github.com/Sora-Labs2026/SoraFiles" rel="noopener noreferrer"&gt;https://github.com/Sora-Labs2026/SoraFiles&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DISCLOSURE&lt;/p&gt;

&lt;p&gt;Sora Labs develops SoraFiles.&lt;/p&gt;

&lt;p&gt;SoraFiles is used here as the practical case study for the engineering lessons described in this article.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
