<?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: Bruce Xu</title>
    <description>The latest articles on DEV Community by Bruce Xu (@bruce_xu).</description>
    <link>https://dev.to/bruce_xu</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%2F4196644%2Fbf36aeec-83ee-4f7b-9916-eb02750e45c9.png</url>
      <title>DEV Community: Bruce Xu</title>
      <link>https://dev.to/bruce_xu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bruce_xu"/>
    <language>en</language>
    <item>
      <title>150 browser tools, no server, no uploads — the architecture behind a static toolkit</title>
      <dc:creator>Bruce Xu</dc:creator>
      <pubDate>Wed, 04 Nov 2026 07:53:51 +0000</pubDate>
      <link>https://dev.to/bruce_xu/150-browser-tools-no-server-no-uploads-the-architecture-behind-a-static-toolkit-1h0d</link>
      <guid>https://dev.to/bruce_xu/150-browser-tools-no-server-no-uploads-the-architecture-behind-a-static-toolkit-1h0d</guid>
      <description>&lt;p&gt;I shipped a toolkit of 150 small browser tools. Every one of them runs in the tab: the file is read locally, processed locally, and never sent to a server.&lt;/p&gt;

&lt;p&gt;That's not a marketing line — it's an architectural constraint I imposed on myself, and it made almost every tool harder to build. Here's what it costs and how the pieces actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 150 tools are
&lt;/h2&gt;

&lt;p&gt;Roughly four groups, and each one needed a different processing model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text and dev utilities (the majority).&lt;/strong&gt; JSON/YAML/XML, Base64, JWT, regex, hashes, cron parsing, SQL formatting, diff, CSV ↔ JSON. Pure JS, no wasm, instant. These are easy and they're not why anyone stays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image tools.&lt;/strong&gt; Compress, resize, crop, convert, watermark, collage, OCR. Canvas + &lt;code&gt;OffscreenCanvas&lt;/code&gt; in Web Workers, so the main thread keeps painting at 60 fps while a 12 MP photo is being re-encoded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF tools.&lt;/strong&gt; Merge, split, page ranges, watermark, PDF → images, images → PDF. &lt;code&gt;pdf-lib&lt;/code&gt;, entirely in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio and video.&lt;/strong&gt; Trim, video → GIF, extract audio, screenshots, screen/camera/mic recording. &lt;code&gt;ffmpeg.wasm&lt;/code&gt; in a worker, single-threaded — full write-up of why in a separate post.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why static export, and why it's a feature
&lt;/h2&gt;

&lt;p&gt;The whole site is a Next.js static export on Cloudflare Pages: HTML pages, no server process.&lt;/p&gt;

&lt;p&gt;The performance reason is boring (CDN cache). The privacy reason is the real one: &lt;strong&gt;there is no server to send a file to.&lt;/strong&gt; A tool can't accidentally upload your PDF if the deployment has no runtime that accepts a PDF. That's a property I can't lose by shipping a careless commit, and it's worth more than any privacy policy paragraph.&lt;/p&gt;

&lt;p&gt;The cost is that everything client-side has to fit in a static bundle, which is where the 25 MiB per-asset limit and the wasm-from-CDN compromise come from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data flow, for every tool
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File → &amp;lt;input type=file&amp;gt; → ArrayBuffer (in the tab)
     → wasm / worker / canvas
     → Blob → URL.createObjectURL → &amp;lt;a download&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no fifth step. If you open the Network panel and process a 40 MB video, the only requests you'll see are the page, the JS, and the wasm engine on first use. That's the whole test, and it's why I wrote a separate post on how to audit claims like this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things this architecture cost me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. No sync, no history across devices, no cloud backup.&lt;/strong&gt; No account means no place to put them. Some users ask for it; the honest answer is that adding it would delete the only thing that makes this different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Memory ceilings are real and user-visible.&lt;/strong&gt; A very large video can kill a tab. On phones it's worse. I try to fail with a message rather than a blank screen, and I don't always succeed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The first video use is a 30 MB download.&lt;/strong&gt; Unavoidable given the asset limit, so the UI shows a progress number instead of a spinner and explains what's downloading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Bundle weight on a 150-tool site.&lt;/strong&gt; Every tool page lazy-loads its own chunk, which mostly works, and the entry bundle is small. It's not a small site, and pretending otherwise would be silly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still rough
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The homepage is a wall of 150 tools. I know that's bad IA. Category pages help; the homepage still isn't good.&lt;/li&gt;
&lt;li&gt;Mobile works for text and image tools; the video tools are desktop-first and I'd rather say so than ship a spinner.&lt;/li&gt;
&lt;li&gt;16 languages is a maintenance tax. Every new tool is 16 translations before it's usable, and machine-translated UI copy for technical terms is a permanent quality problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The question I actually want answered
&lt;/h2&gt;

&lt;p&gt;If you had to delete 100 of these 150, which 50 would you keep?&lt;/p&gt;

&lt;p&gt;I'm not being cute. The long tail of "tool #151" costs me more in maintenance than it returns, and I'd rather make a handful genuinely good than keep widening a grid.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Start here if you want to try one: &lt;a href="https://www.wtoolskit.com/en/image-compressor/" rel="noopener noreferrer"&gt;image compressor&lt;/a&gt; — no upload, no signup. Full list at &lt;a href="https://www.wtoolskit.com/en/" rel="noopener noreferrer"&gt;wtoolskit.com&lt;/a&gt;. It installs as a PWA, so it also works with Wi-Fi off.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to check whether a "client-side" web tool really stays client-side</title>
      <dc:creator>Bruce Xu</dc:creator>
      <pubDate>Mon, 02 Nov 2026 09:09:50 +0000</pubDate>
      <link>https://dev.to/bruce_xu/how-to-check-whether-a-client-side-web-tool-really-stays-client-side-295k</link>
      <guid>https://dev.to/bruce_xu/how-to-check-whether-a-client-side-web-tool-really-stays-client-side-295k</guid>
      <description>&lt;p&gt;"Processed in your browser." "Your files never leave your device." "100% client-side."&lt;/p&gt;

&lt;p&gt;Those sentences cost nothing to write. I've audited a dozen tools that say them, and the claim is true for maybe half. The failure mode is rarely a lie — it's usually a half-truth: the &lt;em&gt;preview&lt;/em&gt; is local, the export is uploaded; or the file is uploaded but "only for processing"; or the compression happens in the tab and then the original quietly goes to a CDN for "backup".&lt;/p&gt;

&lt;p&gt;Here are the four checks I run, in the order that catches the most of it fastest. DevTools and about five minutes per tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Watch what leaves the tab
&lt;/h2&gt;

&lt;p&gt;Open DevTools → &lt;strong&gt;Network&lt;/strong&gt;, tick &lt;strong&gt;Disable cache&lt;/strong&gt;, then run one real file through the tool.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filter to &lt;strong&gt;Fetch/XHR&lt;/strong&gt; and sort by &lt;strong&gt;Size&lt;/strong&gt;, descending.&lt;/li&gt;
&lt;li&gt;Look for any &lt;code&gt;POST&lt;/code&gt; / &lt;code&gt;PUT&lt;/code&gt; whose request body is roughly the size of your file (or the sum of several chunked requests matching it).&lt;/li&gt;
&lt;li&gt;Also check the &lt;strong&gt;WS&lt;/strong&gt; filter. An upload over a WebSocket won't show up as an XHR, and it's a common way to hide one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tool that processes locally will show you JS, a wasm binary, maybe fonts — and nothing carrying your file. A tool that uploads will show a request with a payload you can literally see in the &lt;strong&gt;Payload&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;Two things that make this check lie to you: an upload that's split into 64 KB chunks looks small in every individual row (sum them), and an upload triggered only on &lt;em&gt;download&lt;/em&gt; looks clean until you click export. So do the check twice — once on import, once on export.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Turn the network off
&lt;/h2&gt;

&lt;p&gt;Not "throttle to offline" — actually turn Wi-Fi off, then reload.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page still loads and the tool still works → the processing really is local, or at minimum the whole pipeline is cached.&lt;/li&gt;
&lt;li&gt;Page loads from cache but the tool hangs on "processing" → the engine is remote. That's the single loudest signal you'll get.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat: a PWA that cached its shell will load offline even if its engine is a server API. That's why check 2 never stands alone — it's a filter, not a verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Count the third-party requests
&lt;/h2&gt;

&lt;p&gt;This is where "local" starts needing a definition, and where most audits stop too early.&lt;/p&gt;

&lt;p&gt;Open the &lt;strong&gt;Network&lt;/strong&gt; panel on a cold load (hard reload, cache disabled) and look at the &lt;strong&gt;Domain&lt;/strong&gt; column. What you'll typically find:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request type&lt;/th&gt;
&lt;th&gt;Does it leak the file?&lt;/th&gt;
&lt;th&gt;Does it leak you?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your page + JS bundle&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;IP, UA, Referer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wasm engine from a public CDN&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;IP, UA — &lt;strong&gt;and the CDN operator sees which tool you're using&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Fonts / analytics&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;IP, UA, page, sometimes a client id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad script&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;IP, UA, plus whatever the ad network fingerprints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;POST&lt;/code&gt; with your file in it&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So: "your file never leaves your device" and "this page makes zero network requests" are &lt;strong&gt;different claims&lt;/strong&gt;. A tool can honestly make the first and still phone home constantly. When a tool's privacy page conflates the two, that's not proof of bad faith, but it is a reason to run check 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Look for the machinery
&lt;/h2&gt;

&lt;p&gt;If processing is local, something in the tab has to do the work. Local processing has a fingerprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;&lt;code&gt;.wasm&lt;/code&gt;&lt;/strong&gt; file (ffmpeg, sql.js, ImageMagick, OpenCV — anything heavy),&lt;/li&gt;
&lt;li&gt;or a &lt;strong&gt;Web Worker&lt;/strong&gt; (&lt;code&gt;worker.js&lt;/code&gt; in the Network panel, or a &lt;code&gt;blob:&lt;/code&gt; worker),&lt;/li&gt;
&lt;li&gt;or hot &lt;strong&gt;canvas / &lt;code&gt;OffscreenCanvas&lt;/code&gt;&lt;/strong&gt; usage for image work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find none of those and the only thing that happens when you hit "process" is a &lt;code&gt;fetch('/api/...')&lt;/code&gt;, you're looking at a server. No wasm, no worker, no canvas, but a &lt;code&gt;2.4 MB → 210 KB&lt;/code&gt; result — that's arithmetic happening somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three fake-local patterns
&lt;/h2&gt;

&lt;p&gt;These are the ones I keep meeting. All three can claim "client-side" without technically lying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Local preview, remote export.&lt;/strong&gt; The crop/trim UI is local (it's just CSS and a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element), the actual encode is an upload. Check: does the &lt;em&gt;export&lt;/em&gt; button fire a request?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Local processing, remote copy.&lt;/strong&gt; The file is compressed in the tab and also sent to storage "so you don't lose it". Check: is there a &lt;code&gt;PUT&lt;/code&gt; to somewhere that isn't your own download?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Local engine, remote data.&lt;/strong&gt; The wasm runs in the tab, but the file is passed to a third-party API for OCR / transcription / "AI enhance". Check: any &lt;code&gt;POST&lt;/code&gt; after the wasm finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest boundary
&lt;/h2&gt;

&lt;p&gt;I'd rather state the limit of this method than oversell it.&lt;/p&gt;

&lt;p&gt;Local processing does &lt;strong&gt;not&lt;/strong&gt; mean zero network requests. The page, the JS, the wasm, the fonts, and any analytics or ad script all reveal your IP and user agent. What "local" should mean is narrower and testable: &lt;strong&gt;the bytes of your file are read by code running in your tab, and no request carries them anywhere.&lt;/strong&gt; That's the claim worth checking, and all four checks above test exactly that.&lt;/p&gt;

&lt;p&gt;If you want to verify a specific tool, the fastest route is usually its own documentation of third-party requests. A tool that lists them is telling you it expects to be checked; a tool that only says "we care about privacy" is telling you it doesn't.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I build one of these — a set of 117 browser tools at &lt;a href="https://www.wtoolskit.com/en/" rel="noopener noreferrer"&gt;wtoolskit.com&lt;/a&gt;. Free, no account. Don't take my word for it: the four checks above are exactly how I'd audit it, and the network panel doesn't care who wrote the page.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Suite of 100+ Dev Tools That Runs 100% in the Browser</title>
      <dc:creator>Bruce Xu</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:37:26 +0000</pubDate>
      <link>https://dev.to/bruce_xu/how-i-built-a-suite-of-100-dev-tools-that-runs-100-in-the-browser-2b4f</link>
      <guid>https://dev.to/bruce_xu/how-i-built-a-suite-of-100-dev-tools-that-runs-100-in-the-browser-2b4f</guid>
      <description>&lt;p&gt;A few months ago, I was inspecting a JWT token using a popular online utility when I noticed network requests firing off every time I pasted a string into the input box.&lt;/p&gt;

&lt;p&gt;That moment made me uncomfortable. Why do we send sensitive environment variables, API payloads, and tokens over the wire just to run standard string manipulation or formatting?&lt;/p&gt;

&lt;p&gt;That inspired me to build &lt;strong&gt;&lt;a href="https://www.wtoolskit.com/" rel="noopener noreferrer"&gt;WToolsKit&lt;/a&gt;&lt;/strong&gt; — a pure frontend platform providing &lt;strong&gt;100+ utilities&lt;/strong&gt; that run entirely inside the client browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ The Architecture: Pure Client-Side Magic
&lt;/h2&gt;

&lt;p&gt;To eliminate server dependencies, cloud bills, and privacy risks, I relied heavily on modern browser Web APIs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client-Side Compression&lt;/strong&gt;: Using HTML5 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; and &lt;code&gt;FileReader&lt;/code&gt; APIs to manipulate raw image data directly without sending files to an API endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Network Overhead&lt;/strong&gt;: Formatting JSON or calculating text diffs happens in memory via Web Workers/pure JS functions, making execution instantaneous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline First Capability&lt;/strong&gt;: Since there is no backend API, once the static assets load, tools function flawlessly even on slow or unreliable networks.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🚀 Key Tool Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Developer Tools&lt;/strong&gt;: JSON Validator, RegEx Debugger, JWT Decoder, Base64/URL Encoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design Helpers&lt;/strong&gt;: Color Picker, CSS Shadow/Gradient Generators, SVG Cleaners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converters &amp;amp; Formatters&lt;/strong&gt;: Text Diff, Markdown Preview, Image Format Converters.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔗 Check It Out
&lt;/h2&gt;

&lt;p&gt;You can explore the full collection of tools at &lt;strong&gt;&lt;a href="https://www.wtoolskit.com/" rel="noopener noreferrer"&gt;https://www.wtoolskit.com/&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're a fellow builder, I'd love to hear your thoughts on pure browser-based web tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you handle sensitive data when debugging on web utilities?&lt;/li&gt;
&lt;li&gt;What other browser-native APIs do you think are underutilized for dev tools?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>privacy</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
