<?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: Max</title>
    <description>The latest articles on DEV Community by Max (@orthogonalinfo).</description>
    <link>https://dev.to/orthogonalinfo</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%2F3847175%2F78878eb1-022c-4880-ba72-cde851bc87d8.png</url>
      <title>DEV Community: Max</title>
      <link>https://dev.to/orthogonalinfo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/orthogonalinfo"/>
    <language>en</language>
    <item>
      <title>Why Math.random() Is a Security Bug in Password Generators (and the Web Crypto Fix)</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Thu, 11 Jun 2026 17:04:37 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/why-mathrandom-is-a-security-bug-in-password-generators-and-the-web-crypto-fix-3li4</link>
      <guid>https://dev.to/orthogonalinfo/why-mathrandom-is-a-security-bug-in-password-generators-and-the-web-crypto-fix-3li4</guid>
      <description>&lt;p&gt;Last week I was reviewing a small auth service and found this one-liner generating password-reset tokens:&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&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="nx"&gt;CHARS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CHARS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&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;p&gt;It runs. It produces things like &lt;code&gt;xK9$mLp2@nQ7vR4w&lt;/code&gt;. It also happens to be a real security bug.&lt;/p&gt;

&lt;p&gt;That exact pattern is the one I deliberately avoided when I built a small browser-only password generator — and the reason is worth a few hundred words, because almost every "roll your own" password snippet on the web gets it wrong in the &lt;em&gt;same&lt;/em&gt; way. Here's what's broken about &lt;code&gt;Math.random()&lt;/code&gt; for secrets, the fix, and the two gotchas that bite people who try to fix it themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Math.random()&lt;/code&gt; is predictable by design
&lt;/h2&gt;

&lt;p&gt;In V8 — the engine behind Chrome and Node — &lt;code&gt;Math.random()&lt;/code&gt; has used an algorithm called &lt;strong&gt;xorshift128+&lt;/strong&gt; since version 4.9.40 (late 2015). It has 128 bits of internal state, a period of 2^128 − 1, and it passes the TestU01 statistical suite. Statistically, the numbers &lt;em&gt;look&lt;/em&gt; random.&lt;/p&gt;

&lt;p&gt;But "looks random" and "unpredictable" are different properties.&lt;/p&gt;

&lt;p&gt;xorshift128+ is a &lt;strong&gt;pseudo&lt;/strong&gt;-random generator: every output is a deterministic function of that 128-bit state, and the state is recoverable. Feed enough consecutive outputs into a system of linear equations and you can solve for the internal state — there are public tools on GitHub that recover it from as few as &lt;strong&gt;64–128 consecutive&lt;/strong&gt; &lt;code&gt;Math.random()&lt;/code&gt; calls. Once an attacker has the state, every future output is known. Every "random" password you generate after that point is predictable.&lt;/p&gt;

&lt;p&gt;For a UI animation or a Monte Carlo sim, who cares. For a password, an API key, or a session token, that's the whole ballgame.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; is the actual fix
&lt;/h2&gt;

&lt;p&gt;Browsers ship a cryptographically secure RNG (CSPRNG) through the Web Crypto API. It pulls from the OS entropy pool (&lt;code&gt;/dev/urandom&lt;/code&gt; on Linux, &lt;code&gt;BCryptGenRandom&lt;/code&gt; on Windows) and is built so that observing past output tells you nothing about future output. There's no recoverable internal state to solve for.&lt;/p&gt;

&lt;p&gt;The core is four lines:&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;function&lt;/span&gt; &lt;span class="nf"&gt;secureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&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="nx"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;arr&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="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;max&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;Read a fresh 32-bit unsigned integer from the CSPRNG, reduce it into the range you need, done. Swap &lt;code&gt;Math.random()&lt;/code&gt; for this and the prediction attack above is gone.&lt;/p&gt;

&lt;p&gt;But notice that &lt;code&gt;% max&lt;/code&gt; — that's gotcha number one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: modulo bias is real (but size matters)
&lt;/h2&gt;

&lt;p&gt;When you take a random integer modulo your alphabet size, the ranges usually don't divide evenly, so some characters come up more often than others. I wanted to see how bad it actually is, so I generated &lt;strong&gt;6.2 million random bytes&lt;/strong&gt; and bucketed &lt;code&gt;byte % 62&lt;/code&gt; (a typical alphanumeric set):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expected per character: 100,000&lt;/li&gt;
&lt;li&gt;lowest-frequency char: ~96,900 hits&lt;/li&gt;
&lt;li&gt;highest-frequency char: ~121,400 hits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ratio: 1.25&lt;/strong&gt; — a 25% skew&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It happens because &lt;code&gt;256 % 62 = 8&lt;/code&gt;, so byte values 0–7 each give one extra shot to the first eight characters.&lt;/p&gt;

&lt;p&gt;The textbook fix is &lt;strong&gt;rejection sampling&lt;/strong&gt;: throw away any byte in the biased tail and draw again. Rejecting values ≥ 248 dropped the skew to a 1.02 ratio in my test, at the cost of discarding about 3.1% of draws.&lt;/p&gt;

&lt;p&gt;But here's the part the "always use rejection sampling" advice skips: &lt;strong&gt;the bias depends entirely on how big your random integer is relative to the alphabet.&lt;/strong&gt; If you don't read a single byte but a full &lt;code&gt;Uint32&lt;/code&gt; (range 0 to ~4.29 billion), then for a 94-character symbol set, &lt;code&gt;Uint32 % 94&lt;/code&gt; makes the favored characters more likely by roughly &lt;strong&gt;1 part in 45 million&lt;/strong&gt; — a bias of 0.0000022%.&lt;/p&gt;

&lt;p&gt;For a password, that's noise far below anything that matters. So you can skip rejection sampling on purpose and keep the code simple, &lt;em&gt;because a 32-bit draw already makes the bias irrelevant&lt;/em&gt;. If you're minting cryptographic keys, add the rejection step; for human passwords, a wide draw is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 2: the 64KB quota wall
&lt;/h2&gt;

&lt;p&gt;The second surprise showed up while running that bias test. My first attempt asked &lt;code&gt;getRandomValues()&lt;/code&gt; to fill one big buffer:&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="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;620000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// QuotaExceededError: The requested length exceeds 65,536 bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getRandomValues()&lt;/code&gt; refuses any request over &lt;strong&gt;65,536 bytes (64 KB)&lt;/strong&gt; in a single call. It's in the spec and every browser enforces it. If you're generating one 16-character password you'll never hit it, but the moment you batch-generate or fill a large buffer, you have to chunk:&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;function&lt;/span&gt; &lt;span class="nf"&gt;fillSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;65536&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;Undocumented in most tutorials, and a hard failure rather than a silent one — which is at least honest of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why browser-only matters here
&lt;/h2&gt;

&lt;p&gt;A password generator that does the work &lt;strong&gt;server-side&lt;/strong&gt; is a service that has seen your password in plaintext. The only design that makes sense for a secret is to build it on the user's machine, from their OS entropy, so it never touches a network. Open dev tools, watch the Network tab while you click generate, and you should see exactly zero requests.&lt;/p&gt;

&lt;p&gt;If you want to poke at a working version, here's the &lt;a href="https://orthogonal.info/free-password-generator-online/" rel="noopener noreferrer"&gt;browser-only password generator&lt;/a&gt; I built around these exact decisions — everything runs client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  One layer is never enough
&lt;/h2&gt;

&lt;p&gt;A strong, truly-random password fixes the "guessable" problem. It does &lt;strong&gt;nothing&lt;/strong&gt; about phishing, reused credentials, or a leaked database. Generate unique passwords, store them in a real manager, and gate the important accounts with hardware 2FA. Three cheap layers beat one strong one.&lt;/p&gt;

&lt;p&gt;The lesson I keep relearning: in security, the code that "works" and the code that's &lt;em&gt;correct&lt;/em&gt; are often the same length and completely different.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Math.random()&lt;/code&gt; works. &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; is correct.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;How do you handle modulo bias in your own token/ID generators — always reject, or do you size the draw so it doesn't matter? Curious what others do in practice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>How to Compress Images From the Command Line (and in CI) — No Upload, No Account</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Tue, 09 Jun 2026 15:24:08 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/how-to-compress-images-from-the-command-line-and-in-ci-no-upload-no-account-481m</link>
      <guid>https://dev.to/orthogonalinfo/how-to-compress-images-from-the-command-line-and-in-ci-no-upload-no-account-481m</guid>
      <description>&lt;p&gt;Most "compress your images" advice ends with &lt;em&gt;"...now drag your files into this website."&lt;/em&gt; That's fine for a one-off. It's useless when you have a &lt;code&gt;/public/images&lt;/code&gt; folder with 300 PNGs, or a build step that should never ship a 4 MB hero image again.&lt;/p&gt;

&lt;p&gt;I wanted image compression that lives where the rest of my tooling lives: the terminal and CI. No upload, no account, no clicking. Here's the workflow I landed on, plus a tiny CLI I built to make it one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with web-based compressors in a dev workflow
&lt;/h2&gt;

&lt;p&gt;TinyPNG, Squoosh, and friends are great tools. But in a real project they have three issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They don't script.&lt;/strong&gt; You can't put "open a browser and drag files" in a &lt;code&gt;package.json&lt;/code&gt; or a GitHub Action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They upload.&lt;/strong&gt; For a lot of teams, shipping customer/product images to a third-party server is a non-starter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They're one-at-a-time-ish.&lt;/strong&gt; Batch + recursive folders + keeping structure is exactly the boring part you want automated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What you actually want: &lt;code&gt;compress ./images&lt;/code&gt; → done, locally, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: raw &lt;code&gt;sharp&lt;/code&gt; in a script
&lt;/h2&gt;

&lt;p&gt;If you just want the engine, &lt;a href="https://sharp.pixelplumbing.com/" rel="noopener noreferrer"&gt;&lt;code&gt;sharp&lt;/code&gt;&lt;/a&gt; (libvips bindings) is the workhorse. A minimal batch script:&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;// compress.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sharp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;glob&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;glob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&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;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;images/**/*.{jpg,jpeg,png}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dist&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="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for &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;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&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="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;withoutEnlargement&lt;/span&gt;&lt;span class="p"&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="nf"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;✓&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&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 works. But you'll quickly want flags (quality, format, max-width), parallelism across cores, "don't enlarge," metadata stripping, dry-run, and preserved folder structure — and now you're maintaining a tool instead of shipping your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: a tiny CLI that already does all that
&lt;/h2&gt;

&lt;p&gt;So I packaged exactly that into a small, MIT-licensed CLI called &lt;strong&gt;QuickShrink&lt;/strong&gt;. It's a thin, well-tested wrapper over &lt;code&gt;sharp&lt;/code&gt;, focused on the batch-folder workflow. Run it once with &lt;code&gt;npx&lt;/code&gt;, no global install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# compress every image in ./images → ./compressed&lt;/span&gt;
npx https://quickshrink.orthogonal.info/cli/quickshrink.tgz ./images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convert a whole folder to WebP and cap the width for web (the single most impactful thing you can do for page weight):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx https://quickshrink.orthogonal.info/cli/quickshrink.tgz ./photos &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; ./web &lt;span class="nt"&gt;--format&lt;/span&gt; webp &lt;span class="nt"&gt;--max-width&lt;/span&gt; 1600 &lt;span class="nt"&gt;--quality&lt;/span&gt; 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recurse into subfolders and keep the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx https://quickshrink.orthogonal.info/cli/quickshrink.tgz ./assets &lt;span class="nt"&gt;-o&lt;/span&gt; ./out &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preview before you touch anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx https://quickshrink.orthogonal.info/cli/quickshrink.tgz ./photos &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ✓ out/hero.webp        1.38 MB → 42.7 KB  (-97%)
  ✓ out/sub/banner.webp  3.10 MB → 31.1 KB  (-99%)

Done: 2 ok, 0 failed.
Total: 4.47 MB → 73.8 KB  (saved 4.40 MB, 98.4%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flags it supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-o, --out &amp;lt;dir&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Output directory (default &lt;code&gt;./compressed&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--format &amp;lt;fmt&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jpeg&lt;/code&gt; \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--quality &amp;lt;1-100&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Encoder quality (default 80)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--max-width&lt;/code&gt; / &lt;code&gt;--max-height&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Resize down, never up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--recursive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Walk subfolders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--dry-run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show the plan, write nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything runs &lt;strong&gt;locally&lt;/strong&gt; — your images never leave the machine. It uses all your CPU cores and strips metadata by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: wire it into CI
&lt;/h2&gt;

&lt;p&gt;Because it's a single command, dropping it into a GitHub Action is trivial. Here's a step that compresses everything under &lt;code&gt;public/images&lt;/code&gt; and fails loudly if compression errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/images.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compress images&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public/images/**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;shrink&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;20&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compress images&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npx -y https://quickshrink.orthogonal.info/cli/quickshrink.tgz \&lt;/span&gt;
            &lt;span class="s"&gt;./public/images -o ./public/images --format webp --max-width 1600&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit if changed&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name "image-bot"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "bot@users.noreply.github.com"&lt;/span&gt;
          &lt;span class="s"&gt;git add -A&lt;/span&gt;
          &lt;span class="s"&gt;git diff --cached --quiet || git commit -m "chore: compress images"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now nobody on the team can accidentally ship a 4 MB screenshot again. The bot quietly WebP's and resizes on every PR that touches images.&lt;/p&gt;

&lt;h2&gt;
  
  
  A package.json shortcut
&lt;/h2&gt;

&lt;p&gt;For local use, alias it so teammates don't need to remember the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"images"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx -y https://quickshrink.orthogonal.info/cli/quickshrink.tgz ./src/assets -o ./public/img --format webp --max-width 1600"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm run images&lt;/code&gt; and you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about serverless / "I can't install native deps"?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sharp&lt;/code&gt; ships prebuilt binaries, so the CLI works in most CI runners and locally. The one place it gets awkward is constrained serverless functions or runtimes where native libs are a pain. For that case I'm building a small &lt;strong&gt;hosted compression API&lt;/strong&gt; (key-based, metered) so you can &lt;code&gt;POST&lt;/code&gt; an image and get bytes back without bundling libvips. It's in private beta — if that's your use case, there's a note + email on the &lt;a href="https://quickshrink.orthogonal.info/cli/" rel="noopener noreferrer"&gt;CLI page&lt;/a&gt; and I'd genuinely like the feedback on what limits/pricing make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer a GUI for one-offs?
&lt;/h2&gt;

&lt;p&gt;For the occasional "just shrink this one screenshot" moment, there's a &lt;a href="https://quickshrink.orthogonal.info/" rel="noopener noreferrer"&gt;browser version&lt;/a&gt; that does the same thing client-side (the compression runs in your browser via Canvas — also no upload). But for anything repeatable, the CLI is the move.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx https://quickshrink.orthogonal.info/cli/quickshrink.tgz ./images &lt;span class="nt"&gt;--format&lt;/span&gt; webp &lt;span class="nt"&gt;--max-width&lt;/span&gt; 1600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local, scriptable, batch, free, MIT. That's the whole pitch. If you put it in CI, I'd love to hear how it goes — and what flag you wish it had next.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your Online SQL Formatter Might Be Logging Your Database Password</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Thu, 04 Jun 2026 17:03:46 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/your-online-sql-formatter-might-be-logging-your-database-password-3n9p</link>
      <guid>https://dev.to/orthogonalinfo/your-online-sql-formatter-might-be-logging-your-database-password-3n9p</guid>
      <description>&lt;p&gt;Last month I watched a contractor paste a full Kubernetes secret manifest — base64 blobs and all — into the first "free YAML validator" that came up on Google. He just wanted to check the indentation. What he actually did was POST a production database password to a server he'd never heard of, run by people he'll never meet, with a privacy policy he didn't read.&lt;/p&gt;

&lt;p&gt;That's the part of online dev tools nobody talks about.&lt;/p&gt;

&lt;p&gt;A SQL formatter, a YAML validator, a JSON beautifier — they &lt;em&gt;feel&lt;/em&gt; disposable, like a calculator. But a huge number of them send whatever you paste to a backend for processing. If that paste contains a connection string, an API key, or a customer record, you just leaked it. No breach required. You handed it over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "format my SQL" is a data exfiltration path
&lt;/h2&gt;

&lt;p&gt;Here's the mechanic. Server-side tools work like this: your text goes into a &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;, JavaScript fires an HTTP request to &lt;code&gt;/api/format&lt;/code&gt;, the server runs the actual formatting, and the result comes back. Simple to build, which is exactly why so many sites do it that way.&lt;/p&gt;

&lt;p&gt;The problem is what travels in that request body.&lt;/p&gt;

&lt;p&gt;I tested a handful of popular online formatters with my browser's Network tab open. Several of them sent the &lt;strong&gt;entire input payload&lt;/strong&gt; to their own domain. One sent it to a third-party API. The query I pasted was harmless test data, but the request was real — my text left my machine.&lt;/p&gt;

&lt;p&gt;Now picture the realistic version. You're debugging a failing migration at 11pm. You copy the offending query straight out of your ORM logs to "just clean it up." That query has a hardcoded credential a teammate left in six months ago. You paste, you format, you move on.&lt;/p&gt;

&lt;p&gt;The credential is now in someone's request logs, maybe their analytics, maybe an LLM training pipeline if the tool resells data. You will never know.&lt;/p&gt;

&lt;p&gt;This isn't paranoia. It's the same threat model that makes pasting code into random pastebins a fireable offense at most security-conscious shops. We just don't apply it to "format" tools because they feel too small to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually verify a tool is client-side
&lt;/h2&gt;

&lt;p&gt;Don't take any tool's word for it — including the one I'm about to mention. Verifying is a two-minute job and every developer should know how.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Watch the Network tab.&lt;/strong&gt; Open DevTools (F12), go to the Network panel, clear it, then paste your text and hit format. If you see a new XHR/fetch request fire with your input in the payload, the tool is server-side. If nothing happens on the network, the work is local.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;// What a server-side formatter looks like in the Network tab:
POST /api/format-sql
Request Payload:
{ "query": "SELECT * FROM users WHERE token='sk_live_...'" }

// What a client-side tool looks like:
// (nothing — no request fires when you click format)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Kill your connection.&lt;/strong&gt; The bluntest test there is. Load the page, then turn off Wi-Fi or drop into airplane mode. If the tool still formats your text, it's running entirely in the browser. If it spins or errors, it needed a server. I do this with any tool before I trust it with anything sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check for a service worker.&lt;/strong&gt; Truly offline-capable tools register a service worker so they work with no connection at all. In DevTools, look under &lt;strong&gt;Application → Service Workers&lt;/strong&gt;. Its presence is a strong signal the developer designed for offline-first, which usually means client-side processing too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in a real workflow
&lt;/h2&gt;

&lt;p&gt;A few concrete cases where I reach for browser-only tools &lt;em&gt;specifically&lt;/em&gt; because of the data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reviewing a teammate's config PR.&lt;/strong&gt; Diffing two Helm values files that contain registry credentials — done locally, nothing logged anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaning up a query from prod logs.&lt;/strong&gt; Format it to read it, without shipping whatever sensitive &lt;code&gt;WHERE&lt;/code&gt; clause it carries to a stranger's server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validating a CI secrets file.&lt;/strong&gt; Checking that a GitHub Actions YAML parses before you commit, without exposing the encrypted values to a validation API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a locked-down network.&lt;/strong&gt; Some client environments block external dev-tool domains entirely. Offline-capable tools just keep working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The broader point: treat every "paste your text here" box as a potential outbound network call until you've proven otherwise. Most of the time it's fine. The one time it isn't, it's a leaked credential you can't un-leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structural fix
&lt;/h2&gt;

&lt;p&gt;The fix is structural, not procedural. Don't rely on &lt;em&gt;remembering&lt;/em&gt; to scrub secrets first — use tools that physically can't send your data anywhere, because all the work happens in your tab.&lt;/p&gt;

&lt;p&gt;That's the reason I built &lt;a href="https://orthogonal.info/free-sql-formatter-online/" rel="noopener noreferrer"&gt;our formatters&lt;/a&gt; as single-file, client-side apps — a &lt;a href="https://orthogonal.info/free-sql-formatter-online/" rel="noopener noreferrer"&gt;SQL Formatter&lt;/a&gt;, a &lt;a href="https://orthogonal.info/free-yaml-validator-formatter-online/" rel="noopener noreferrer"&gt;YAML Validator&lt;/a&gt;, and a &lt;a href="https://orthogonal.info/free-diff-checker-online/" rel="noopener noreferrer"&gt;Diff Checker&lt;/a&gt; where the parsing runs in JavaScript on your device. There is no &lt;code&gt;/api/format&lt;/code&gt; endpoint. The text in your textarea never crosses the network because there's nowhere for it to go.&lt;/p&gt;

&lt;p&gt;But browser-only tools only remove &lt;em&gt;one&lt;/em&gt; exfiltration path. Defense in depth still applies: rotate the credentials that have already been pasted into who-knows-what, and put a &lt;a href="https://orthogonal.info/i-caught-14-leaked-secrets-in-my-git-history-heres-the-pre-commit-setup-that-stops-it/" rel="noopener noreferrer"&gt;pre-commit secret scanner&lt;/a&gt; in front of your repos so the hardcoded ones never ship in the first place.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;How do you handle this on your team — do you have a policy on pasting into online dev tools, or is it the wild west? Curious what threat models other people apply here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Made an Image Compressor That Never Sees Your Images (100% Client-Side)</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Fri, 29 May 2026 15:02:23 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/i-made-an-image-compressor-that-never-sees-your-images-100-client-side-309e</link>
      <guid>https://dev.to/orthogonalinfo/i-made-an-image-compressor-that-never-sees-your-images-100-client-side-309e</guid>
      <description>&lt;p&gt;Ever notice how most "free" image compressors upload your files to their servers?&lt;/p&gt;

&lt;p&gt;I got fed up with this — especially when compressing screenshots that might contain sensitive data — so I built &lt;strong&gt;QuickShrink&lt;/strong&gt;: an image compressor that runs entirely in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Privacy: Your images never leave your device&lt;/li&gt;
&lt;li&gt;Speed: No upload/download round-trip. Compression is instant&lt;/li&gt;
&lt;li&gt;Works offline: It's a PWA. Install it and use it without internet&lt;/li&gt;
&lt;li&gt;No account needed: Just drag, drop, done&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Compress JPEG/PNG/WebP with adjustable quality (1-100)&lt;/li&gt;
&lt;li&gt;Batch compress multiple images at once&lt;/li&gt;
&lt;li&gt;Convert between formats (PNG to WebP for 60-80% size reduction)&lt;/li&gt;
&lt;li&gt;Resize images with aspect ratio lock&lt;/li&gt;
&lt;li&gt;See before/after comparison with file sizes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tech
&lt;/h2&gt;

&lt;p&gt;Built with vanilla JavaScript + Canvas API + OffscreenWorker for non-blocking compression. The entire app is about 50KB. No React, no framework, no build step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;quickshrink.orthogonal.info&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free tier: 5 compressions/day&lt;br&gt;
Pro: Unlimited + batch + format conversion ($4.99 one-time)&lt;/p&gt;

&lt;p&gt;I'd love feedback on compression quality vs file size. Is the default quality slider (80%) a good default for web images?&lt;/p&gt;

&lt;p&gt;Built this as a weekend project after TinyPNG's free tier dropped to 20 images/month. If you're compressing screenshots or blog images regularly, give it a shot.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Why I Stopped Uploading Images to Compression Services (And Built a Browser-Only Alternative)</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Thu, 28 May 2026 17:01:55 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/why-i-stopped-uploading-images-to-compression-services-and-built-a-browser-only-alternative-1n1o</link>
      <guid>https://dev.to/orthogonalinfo/why-i-stopped-uploading-images-to-compression-services-and-built-a-browser-only-alternative-1n1o</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every web developer has been there: you need to compress images for a client project, so you drag them to TinyPNG or some random compression site.&lt;/p&gt;

&lt;p&gt;But then you pause. These are mockups with unreleased branding. Or screenshots with sensitive data. Or client photos under NDA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you really want to upload them to a third-party server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I started thinking about this more seriously after GDPR enforcement got real. If you're processing client images through external services, that's technically data processing you need to account for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Browser Can Do This Now
&lt;/h2&gt;

&lt;p&gt;Modern browsers ship with Canvas API, OffscreenCanvas, and WebAssembly support that makes client-side image processing genuinely fast. We're not talking about the janky Canvas-based compression from 2015 — WASM codecs for WebP and AVIF run at near-native speed now.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;your browser is already an image processing engine&lt;/strong&gt;. It decodes every image you view. Why upload somewhere else just to re-encode it smaller?&lt;/p&gt;

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

&lt;p&gt;I made &lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;QuickShrink&lt;/a&gt; — a free image compressor that runs 100% in your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag and drop (or paste from clipboard)&lt;/li&gt;
&lt;li&gt;Images never leave your machine&lt;/li&gt;
&lt;li&gt;No accounts, no limits, no upsells&lt;/li&gt;
&lt;li&gt;Handles PNG, JPEG, WebP&lt;/li&gt;
&lt;li&gt;Batch processing supported&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire thing is static HTML/JS. You could literally save the page and run it offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Approach
&lt;/h2&gt;

&lt;p&gt;The compression pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read file as ArrayBuffer&lt;/li&gt;
&lt;li&gt;Decode to ImageBitmap (hardware-accelerated)&lt;/li&gt;
&lt;li&gt;Draw to OffscreenCanvas at target quality&lt;/li&gt;
&lt;li&gt;Export as compressed blob&lt;/li&gt;
&lt;li&gt;Trigger download&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most photos, you get 60-80% size reduction at quality 0.8 with zero perceptible difference. For PNGs with transparency, re-encoding as WebP typically saves 40-60%.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You Should Still Use Server-Side
&lt;/h2&gt;

&lt;p&gt;To be fair, client-side isn't always the answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build pipelines&lt;/strong&gt;: Use Sharp/libvips in your CI for automated optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN transforms&lt;/strong&gt;: Cloudflare/Imgix handle format negotiation per-device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk processing&lt;/strong&gt;: 10,000 images? Use a proper batch tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for the everyday "I need to shrink these 5 screenshots before adding them to the README" — a browser tool is faster and more private than any upload-based service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;quickshrink.orthogonal.info&lt;/a&gt; — open source, free forever, no tracking.&lt;/p&gt;

&lt;p&gt;Would love feedback from folks who've built similar tools. What codecs are you using? Anyone gotten JPEG XL working reliably in-browser yet?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, I write about web performance and developer tools at &lt;a href="https://orthogonal.info" rel="noopener noreferrer"&gt;orthogonal.info&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webperf</category>
      <category>javascript</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to Optimize Images for Website Speed in 2026 (Without Losing Quality)</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Thu, 28 May 2026 15:03:17 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/how-to-optimize-images-for-website-speed-in-2026-without-losing-quality-56b1</link>
      <guid>https://dev.to/orthogonalinfo/how-to-optimize-images-for-website-speed-in-2026-without-losing-quality-56b1</guid>
      <description>&lt;p&gt;Images account for ~50% of total page weight on most websites. If your site loads slowly, images are almost certainly the bottleneck.&lt;/p&gt;

&lt;p&gt;After years of optimizing web performance, here's my complete workflow for image optimization in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Image Optimization Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core Web Vitals&lt;/strong&gt;: Google uses LCP (Largest Contentful Paint) as a ranking signal. Unoptimized hero images = slow LCP = lower rankings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounce rate&lt;/strong&gt;: 53% of mobile users leave if a page takes &amp;gt;3 seconds to load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth costs&lt;/strong&gt;: Smaller images = less CDN/hosting cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 4-Step Image Optimization Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Choose the Right Format
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Browser Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;Photos, illustrations&lt;/td&gt;
&lt;td&gt;97%+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;Photos (best compression)&lt;/td&gt;
&lt;td&gt;~92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Transparency, icons&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG&lt;/td&gt;
&lt;td&gt;Logos, simple graphics&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt;: Use WebP for everything unless you need transparency (PNG) or have vector graphics (SVG).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Resize Before Compressing
&lt;/h3&gt;

&lt;p&gt;Never serve a 4000px image in a 800px container. Resize first:&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;img&lt;/span&gt; 
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"image-400.webp 400w, image-800.webp 800w, image-1200.webp 1200w"&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"image-800.webp"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"descriptive alt text"&lt;/span&gt;
  &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
  &lt;span class="na"&gt;decoding=&lt;/span&gt;&lt;span class="s"&gt;"async"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Compress Aggressively
&lt;/h3&gt;

&lt;p&gt;Most images look identical at 75-80% quality. The sweet spot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hero images&lt;/strong&gt;: 80-85% quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thumbnails&lt;/strong&gt;: 70-75% quality
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background images&lt;/strong&gt;: 60-70% quality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use &lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;QuickShrink&lt;/a&gt; for this because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It runs entirely in the browser (no upload = no privacy risk)&lt;/li&gt;
&lt;li&gt;Supports batch compression&lt;/li&gt;
&lt;li&gt;Lets you compare before/after side by side&lt;/li&gt;
&lt;li&gt;Works offline once loaded&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Lazy Load Everything Below the Fold
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Above the fold: load immediately --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.webp"&lt;/span&gt; &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Below the fold: lazy load --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"gallery-1.webp"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt; &lt;span class="na"&gt;decoding=&lt;/span&gt;&lt;span class="s"&gt;"async"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick Wins Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Convert all JPEGs to WebP (30-50% smaller)&lt;/li&gt;
&lt;li&gt;[ ] Add &lt;code&gt;loading="lazy"&lt;/code&gt; to images below the fold&lt;/li&gt;
&lt;li&gt;[ ] Add &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes (prevents layout shift)&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;code&gt;srcset&lt;/code&gt; for responsive images&lt;/li&gt;
&lt;li&gt;[ ] Compress to 75-80% quality&lt;/li&gt;
&lt;li&gt;[ ] Add &lt;code&gt;fetchpriority="high"&lt;/code&gt; to LCP image&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tools I Recommend
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;QuickShrink&lt;/a&gt;&lt;/strong&gt; — Free, browser-based, no upload needed. My go-to for quick compression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharp (Node.js)&lt;/strong&gt; — For build pipelines and automation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Squoosh CLI&lt;/strong&gt; — Google's tool for batch processing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real Results
&lt;/h2&gt;

&lt;p&gt;On a recent client site, following this workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page weight: 4.2MB → 890KB (79% reduction)&lt;/li&gt;
&lt;li&gt;LCP: 4.1s → 1.3s&lt;/li&gt;
&lt;li&gt;PageSpeed score: 62 → 94&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Use WebP format&lt;/li&gt;
&lt;li&gt;Resize to display dimensions&lt;/li&gt;
&lt;li&gt;Compress to 75-80% quality (try &lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;QuickShrink&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Lazy load below-the-fold images&lt;/li&gt;
&lt;li&gt;Add width/height to prevent layout shift&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Doing just steps 1-3 typically cuts image size by 70%+.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your image optimization workflow? Drop it in the comments — always looking for new tricks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>webperf</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>TinyPNG vs QuickShrink: Why I Switched to Client-Side Image Compression</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Wed, 27 May 2026 15:03:51 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/tinypng-vs-quickshrink-why-i-switched-to-client-side-image-compression-56b9</link>
      <guid>https://dev.to/orthogonalinfo/tinypng-vs-quickshrink-why-i-switched-to-client-side-image-compression-56b9</guid>
      <description>&lt;p&gt;Every web developer knows TinyPNG. It's been the go-to image compressor for years. But after years of using it, I started questioning: &lt;strong&gt;why am I uploading my images to someone else's server?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Server-Side Compression
&lt;/h2&gt;

&lt;p&gt;Tools like TinyPNG, Compressor.io, and even Squoosh (partially) require you to upload images to their servers. For most casual use, that's fine. But consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NDA projects&lt;/strong&gt; — client mockups, unreleased product photos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medical/legal images&lt;/strong&gt; — patient data, case evidence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal photos&lt;/strong&gt; — family photos you want smaller but private&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise work&lt;/strong&gt; — screenshots of internal dashboards, Slack messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every upload is a data transfer you can't take back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Client-Side Compression?
&lt;/h2&gt;

&lt;p&gt;Instead of sending your image to a server for processing, client-side compression uses your browser's built-in Canvas API and WebP encoding to compress images &lt;strong&gt;locally&lt;/strong&gt;. The file never leaves your device.&lt;/p&gt;

&lt;p&gt;Open DevTools → Network tab → compress an image → &lt;strong&gt;zero outgoing requests&lt;/strong&gt;. That's the proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: TinyPNG vs QuickShrink
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;TinyPNG&lt;/th&gt;
&lt;th&gt;QuickShrink&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;Server-side (images uploaded)&lt;/td&gt;
&lt;td&gt;100% client-side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;td&gt;Yes (20 images, 5MB each)&lt;/td&gt;
&lt;td&gt;Yes (unlimited size)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP output&lt;/td&gt;
&lt;td&gt;No (PNG/JPEG only)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resize&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality presets&lt;/td&gt;
&lt;td&gt;No (auto only)&lt;/td&gt;
&lt;td&gt;Web, Social, Email, Print&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (PWA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Yes ($)&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Free (20/day) or 9/yr&lt;/td&gt;
&lt;td&gt;Free (5/day) or .99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Compression Quality
&lt;/h2&gt;

&lt;p&gt;Let's be honest — TinyPNG's lossy PNG compression is excellent. Their algorithm (quantization + DEFLATE) produces incredibly small PNGs with minimal quality loss.&lt;/p&gt;

&lt;p&gt;QuickShrink uses Canvas API resampling + WebP encoding, which takes a different approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPEG → WebP&lt;/strong&gt;: typically 40-60% smaller at equivalent quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG → WebP&lt;/strong&gt;: typically 50-80% smaller (lossy WebP vs lossless PNG)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JPEG → JPEG&lt;/strong&gt;: 20-40% smaller with quality adjustment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff: TinyPNG gives better PNG-to-PNG results. QuickShrink wins when you convert to WebP (which you should for web use in 2024+).&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Which
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use TinyPNG when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need PNG output specifically&lt;/li&gt;
&lt;li&gt;Images aren't sensitive&lt;/li&gt;
&lt;li&gt;You want the absolute smallest PNG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use QuickShrink when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Privacy matters (NDAs, sensitive content)&lt;/li&gt;
&lt;li&gt;You want WebP output (better web performance)&lt;/li&gt;
&lt;li&gt;You need resize + compress in one step&lt;/li&gt;
&lt;li&gt;You want offline capability&lt;/li&gt;
&lt;li&gt;You don't want to create an account&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;QuickShrink&lt;/a&gt; — free, no signup, works in any modern browser.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your image compression workflow? Still uploading to servers, or have you gone client-side?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>privacy</category>
      <category>tools</category>
    </item>
    <item>
      <title>I Caught 14 Leaked Secrets in My Git History — Here is the Pre-Commit Setup That Stops It</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Tue, 26 May 2026 17:03:43 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/i-caught-14-leaked-secrets-in-my-git-history-here-is-the-pre-commit-setup-that-stops-it-df4</link>
      <guid>https://dev.to/orthogonalinfo/i-caught-14-leaked-secrets-in-my-git-history-here-is-the-pre-commit-setup-that-stops-it-df4</guid>
      <description>&lt;p&gt;Last month I ran &lt;code&gt;trufflehog&lt;/code&gt; against one of my private repos — a homelab automation project I’d never planned to open-source. It found 14 live secrets. AWS keys, a Telegram bot token, two database passwords, and a Stripe test key that still had access to customer data. All committed between 2022 and 2024, scattered across dozens of commits.&lt;/p&gt;

&lt;p&gt;The fix took me about 20 minutes. I now run two tools as pre-commit hooks that catch secrets before they ever reach &lt;code&gt;.git/objects&lt;/code&gt;. Here’s exactly how I set it up, what each tool catches that the other misses, and the one configuration mistake that will give you false confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Two Tools: git-secrets vs trufflehog
&lt;/h2&gt;

&lt;p&gt;I use both &lt;a href="https://github.com/awslabs/git-secrets" rel="noopener noreferrer"&gt;git-secrets&lt;/a&gt; and &lt;a href="https://github.com/trufflesecurity/trufflehog" rel="noopener noreferrer"&gt;trufflehog&lt;/a&gt; because they work differently and catch different things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;git-secrets&lt;/strong&gt; is pattern-based. It ships with AWS-specific patterns out of the box (matches &lt;code&gt;AKIA[0-9A-Z]{16}&lt;/code&gt; and similar) and lets you add custom regexes. It’s fast — sub-100ms on most commits — and runs as a native git hook. The downside: it only knows what you tell it to look for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;trufflehog&lt;/strong&gt; uses entropy detection &lt;em&gt;and&lt;/em&gt; pattern matching. It calculates Shannon entropy on strings and flags anything that looks random enough to be a key. Version 3 also verifies secrets against live APIs — it’ll actually try your AWS key against STS to confirm it’s active. This is slower (2-5 seconds per commit) but catches novel secret formats that pattern matching misses.&lt;/p&gt;

&lt;p&gt;In my 14-secret audit, git-secrets would have caught 9 of them. trufflehog caught all 14. But git-secrets has zero false positives in my workflow, while trufflehog flags about 1 false positive per week on base64-encoded config blobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up git-secrets as a Pre-Commit Hook
&lt;/h2&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install git-secrets   # macOS
# or
git clone https://github.com/awslabs/git-secrets.git
cd git-secrets &amp;amp;&amp;amp; make install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it in your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd your-repo
git secrets --install
git secrets --register-aws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;--register-aws&lt;/code&gt; flag adds patterns for AWS access keys, secret keys, and account IDs. Now add your own patterns for whatever services you use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Telegram bot tokens (numeric:alphanumeric format)
git secrets --add '[0-9]{8,10}:[A-Za-z0-9_-]{35}'

# Stripe keys
git secrets --add 'sk_(live|test)_[A-Za-z0-9]{24,}'

# Generic high-entropy passwords in connection strings
git secrets --add 'password\s*=\s*[^\s]{12,}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo "AKIAIOSFODNN7EXAMPLE" &amp;gt; test.txt
git add test.txt
git commit -m "test"
# Output: [ERROR] Matched one or more prohibited patterns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha: &lt;code&gt;git secrets --install&lt;/code&gt; only sets up hooks in &lt;em&gt;that&lt;/em&gt; repo. For global coverage across all repos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git secrets --install ~/.git-templates/git-secrets
git config --global init.templateDir ~/.git-templates/git-secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding trufflehog as a Pre-Commit Hook
&lt;/h2&gt;

&lt;p&gt;I use the &lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;pre-commit&lt;/a&gt; framework for trufflehog since it handles updates and version pinning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .pre-commit-config.yaml
repos:
  - repo: https://github.com/trufflesecurity/trufflehog
    rev: v3.78.1
    hooks:
      - id: trufflehog
        entry: trufflehog git file://. --since-commit HEAD --only-verified --fail
        stages: [commit, push]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--only-verified&lt;/code&gt; flag is important. Without it, trufflehog reports every high-entropy string — UUIDs, hashes, random test data. With it, you only get alerts for secrets that are confirmed active against their respective APIs. This drops false positives from ~30/week to about 1.&lt;/p&gt;

&lt;p&gt;Install and activate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install pre-commit
pre-commit install
pre-commit install --hook-type pre-push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Configuration Mistake That Gives False Confidence
&lt;/h2&gt;

&lt;p&gt;Here’s what tripped me up for months: &lt;strong&gt;git-secrets only scans staged changes by default, not the full file.&lt;/strong&gt; If you have a secret on line 5 and you modify line 50, git-secrets won’t flag it because line 5 isn’t in the diff.&lt;/p&gt;

&lt;p&gt;This matters because secrets often enter a file in one commit and stay there forever. The pre-commit hook only fires on new changes, so existing secrets remain invisible.&lt;/p&gt;

&lt;p&gt;Fix: run a full-repo scan on a schedule. I have this in a weekly cron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Scan entire repo history
trufflehog git file:///path/to/repo --only-verified --json &amp;gt; /tmp/secrets-audit.json

# Scan all current files (not just diffs)
git secrets --scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I pipe the output to ntfy for notifications. If something shows up, I rotate the credential immediately and use &lt;code&gt;git filter-repo&lt;/code&gt; to purge it from history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git filter-repo --invert-paths --path secrets.env
# Then force-push and tell collaborators to re-clone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What About GitHub’s Built-in Secret Scanning?
&lt;/h2&gt;

&lt;p&gt;GitHub’s secret scanning (free for public repos, paid for private) is solid but it’s a safety net, not prevention. By the time GitHub alerts you, the secret has already been pushed to a remote. If your repo was public for even 5 seconds, bots have already scraped it — I’ve seen AWS keys exploited within &lt;a href="https://www.comparitech.com/blog/information-security/github-honeypot-experiment/" rel="noopener noreferrer"&gt;4 minutes of being pushed&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pre-commit hooks stop the secret locally. That’s the difference between “we caught it early” and “we need to rotate everything and audit CloudTrail logs.”&lt;/p&gt;

&lt;h2&gt;
  
  
  My Full .pre-commit-config.yaml
&lt;/h2&gt;

&lt;p&gt;Here’s what I run on every project now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repos:
  - repo: https://github.com/trufflesecurity/trufflehog
    rev: v3.78.1
    hooks:
      - id: trufflehog
        entry: trufflehog git file://. --since-commit HEAD --only-verified --fail
        stages: [commit, push]

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks
        stages: [commit]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I actually dropped git-secrets from the pre-commit config because gitleaks covers similar patterns with better regex coverage and active maintenance. I still keep git-secrets installed globally as a backup layer — defense in depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total overhead per commit:&lt;/strong&gt; ab&lt;/p&gt;

</description>
      <category>security</category>
      <category>git</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a Free Client-Side Image Compressor (No Upload, Works Offline)</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Tue, 26 May 2026 15:02:21 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/i-built-a-free-client-side-image-compressor-no-upload-works-offline-46f6</link>
      <guid>https://dev.to/orthogonalinfo/i-built-a-free-client-side-image-compressor-no-upload-works-offline-46f6</guid>
      <description>&lt;p&gt;I got tired of uploading sensitive images to servers just to compress them. TinyPNG is great but requires server upload. Squoosh only does one image at a time.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;QuickShrink&lt;/strong&gt; — a browser-based image compressor where everything happens client-side. Your images never leave your device.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-side compression&lt;/strong&gt; — uses Canvas API + WebP encoding in-browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch processing&lt;/strong&gt; — drag multiple files, compress all at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebP output&lt;/strong&gt; — 30-50% smaller than JPEG at same quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resize + compress&lt;/strong&gt; — set max dimensions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality presets&lt;/strong&gt; — Web, Social Media, Email, Print&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PWA&lt;/strong&gt; — install it, works offline&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why client-side matters
&lt;/h2&gt;

&lt;p&gt;If you are compressing screenshots with credentials, internal docs, or client work — you probably do not want them hitting someone elses server. With QuickShrink, the files stay in your browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;quickshrink.orthogonal.info&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free tier: 5 images/day. Pro (4.99/mo): unlimited + priority features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;Vanilla JS, Canvas API, Web Workers for non-blocking compression, Service Worker for offline support. No framework, loads in under 1 second.&lt;/p&gt;




&lt;p&gt;Would love feedback from fellow devs. What features would make this more useful for your workflow?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why I Built a Browser-Only Image Compressor (No Uploads, No Server)</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Tue, 19 May 2026 17:03:03 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/why-i-built-a-browser-only-image-compressor-no-uploads-no-server-1071</link>
      <guid>https://dev.to/orthogonalinfo/why-i-built-a-browser-only-image-compressor-no-uploads-no-server-1071</guid>
      <description>&lt;p&gt;Most image compression tools require you to upload your photos to a remote server. But what if you are compressing screenshots with sensitive data or client mockups under NDA?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Privacy Problem
&lt;/h2&gt;

&lt;p&gt;When you upload to a compression service, you trust that company to not store, not train AI on, and delete your image. You have no way to verify it.&lt;/p&gt;

&lt;p&gt;For personal photos, acceptable risk. For business documents, medical images, legal screenshots? Real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client-Side Compression: How It Works
&lt;/h2&gt;

&lt;p&gt;Modern browsers have powerful image processing APIs (Canvas API, OffscreenCanvas) that can resize and re-encode images entirely in JavaScript:&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;canvas&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Export at reduced quality - never leaves the browser&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&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;// blob is your compressed image&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No fetch calls, no FormData uploads, no server round-trips.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Client-Side vs Server-Side
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Client-side wins when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Images contain confidential information&lt;/li&gt;
&lt;li&gt;You are under NDA or HIPAA compliance&lt;/li&gt;
&lt;li&gt;You want fastest compression (no upload wait)&lt;/li&gt;
&lt;li&gt;Slow or metered internet connection&lt;/li&gt;
&lt;li&gt;You value privacy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Server-side tools may be better when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need advanced algorithms (MozJPEG, AVIF encoding)&lt;/li&gt;
&lt;li&gt;Batch-processing thousands of images via API&lt;/li&gt;
&lt;li&gt;You need specific format conversions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;QuickShrink&lt;/a&gt; to make this dead simple. Drop an image, get a smaller one back. No signup, no uploads, no tracking.&lt;/p&gt;

&lt;p&gt;You can verify yourself: open DevTools Network tab and watch. Zero outbound requests during compression.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What is your approach to image optimization in your projects? Do you trust upload-based tools with sensitive content?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>tools</category>
    </item>
    <item>
      <title>I built a privacy-first image compressor that runs entirely in your browser</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Thu, 14 May 2026 17:02:02 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/i-built-a-privacy-first-image-compressor-that-runs-entirely-in-your-browser-1144</link>
      <guid>https://dev.to/orthogonalinfo/i-built-a-privacy-first-image-compressor-that-runs-entirely-in-your-browser-1144</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every time I needed to compress an image before deploying, I had to choose between:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud tools&lt;/strong&gt; that upload my files to unknown servers (privacy concern with client work)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI tools&lt;/strong&gt; like &lt;code&gt;imagemagick&lt;/code&gt; that require remembering flags every time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid tools&lt;/strong&gt; with freemium limits that kick in at the worst moment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I just wanted something I could bookmark and use in 3 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: QuickShrink
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;QuickShrink&lt;/a&gt; is a free, browser-based image compressor. Zero uploads — all processing happens client-side using the Canvas API and modern compression algorithms.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Drag and drop (or click to upload) any image&lt;/li&gt;
&lt;li&gt;Adjust quality slider if needed&lt;/li&gt;
&lt;li&gt;Download the compressed version&lt;/li&gt;
&lt;li&gt;Your files never leave your machine&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why browser-based matters:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt;: No server ever sees your images. Important for NDA work, client mockups, medical images, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: No upload/download wait. Compression is instant for most files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No limits&lt;/strong&gt;: No daily caps, no signup walls, no watermarks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline&lt;/strong&gt;: Once loaded, it works without internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical bits
&lt;/h2&gt;

&lt;p&gt;The compression uses the browser Canvas API for lossy compression (JPEG quality reduction) and leverages modern browser image codecs. For PNGs, it re-encodes with optimized settings. Typical results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4MB JPEG → ~800KB (80% reduction at quality 80)&lt;/li&gt;
&lt;li&gt;Hero images for blogs → usually 60-70% smaller&lt;/li&gt;
&lt;li&gt;Screenshots with text → 50-60% reduction&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who is this for?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Devs who need quick image compression before &lt;code&gt;git push&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bloggers optimizing hero images&lt;/li&gt;
&lt;li&gt;Anyone working with sensitive/client images who can not use cloud tools&lt;/li&gt;
&lt;li&gt;People tired of signing up for yet another SaaS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;quickshrink.orthogonal.info&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No signup. No ads. Just compression.&lt;/p&gt;




&lt;p&gt;Feedback welcome — what features would make this more useful for your workflow?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>tools</category>
    </item>
    <item>
      <title>Best TinyPNG Alternatives in 2026 (Free &amp; Privacy-First)</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Thu, 07 May 2026 17:03:07 +0000</pubDate>
      <link>https://dev.to/orthogonalinfo/best-tinypng-alternatives-in-2026-free-privacy-first-3n8</link>
      <guid>https://dev.to/orthogonalinfo/best-tinypng-alternatives-in-2026-free-privacy-first-3n8</guid>
      <description>&lt;p&gt;Looking for a &lt;strong&gt;TinyPNG alternative&lt;/strong&gt; that's free, private, and doesn't require uploads? Here are the best options in 2026 — including one that compresses images entirely in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Look Beyond TinyPNG?
&lt;/h2&gt;

&lt;p&gt;TinyPNG is great, but it has limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free tier limited to 20 images/month via API (500 via web)&lt;/li&gt;
&lt;li&gt;Files are uploaded to their servers — privacy concern for sensitive images&lt;/li&gt;
&lt;li&gt;No WebP output from the free web tool&lt;/li&gt;
&lt;li&gt;Pro pricing starts at $25/year&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need more flexibility, better privacy, or different output formats, these alternatives deliver.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. QuickShrink (Best for Privacy)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; Free (5 images/day) | Pro $4.99/mo&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://quickshrink.orthogonal.info" rel="noopener noreferrer"&gt;quickshrink.orthogonal.info&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key advantage:&lt;/strong&gt; 100% browser-based. Your images never leave your device. Zero uploads, zero server processing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports PNG, JPEG, WebP output&lt;/li&gt;
&lt;li&gt;Adjustable quality slider&lt;/li&gt;
&lt;li&gt;Instant results — no waiting for server response&lt;/li&gt;
&lt;li&gt;No account required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Designers and developers who handle client assets or sensitive images and don't want them on third-party servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Squoosh (Google)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; Free&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; squoosh.app&lt;/p&gt;

&lt;p&gt;Google's open-source image compressor. Also browser-based with side-by-side preview. Excellent for single images but no batch processing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced codec options (MozJPEG, AVIF, WebP)&lt;/li&gt;
&lt;li&gt;Real-time quality comparison&lt;/li&gt;
&lt;li&gt;No batch mode — one image at a time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who want fine-grained control over codec settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ShortPixel
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; Free (100 images/month) | From $3.99/mo&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; shortpixel.com&lt;/p&gt;

&lt;p&gt;Server-based compression with WordPress plugin. Good for bulk optimization of existing sites.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress integration&lt;/li&gt;
&lt;li&gt;Lossy, glossy, and lossless modes&lt;/li&gt;
&lt;li&gt;CDN delivery option&lt;/li&gt;
&lt;li&gt;Files uploaded to their servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; WordPress site owners who want set-and-forget optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Compressor.io
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; Free (limited) | Pro $50/year&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; compressor.io&lt;/p&gt;

&lt;p&gt;Clean interface with good compression ratios. Supports SVG compression which most tools don't.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports JPEG, PNG, GIF, SVG, WebP&lt;/li&gt;
&lt;li&gt;Up to 10MB file size&lt;/li&gt;
&lt;li&gt;Server-side processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Designers working with multiple formats including SVG.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. ImageOptim (Mac Only)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; Free (desktop app) | API from $12/mo&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; imageoptim.com&lt;/p&gt;

&lt;p&gt;Desktop app that strips metadata and optimizes locally. Mac-only for the free version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fully offline processing&lt;/li&gt;
&lt;li&gt;Strips EXIF data automatically&lt;/li&gt;
&lt;li&gt;Drag-and-drop batch processing&lt;/li&gt;
&lt;li&gt;Mac only (no Windows/Linux)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Mac users who prefer desktop workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Privacy&lt;/th&gt;
&lt;th&gt;Batch&lt;/th&gt;
&lt;th&gt;Free Limit&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;QuickShrink&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Browser-only&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;td&gt;5/day&lt;/td&gt;
&lt;td&gt;$4.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TinyPNG&lt;/td&gt;
&lt;td&gt;❌ Upload&lt;/td&gt;
&lt;td&gt;✅ 20 files&lt;/td&gt;
&lt;td&gt;500/month web&lt;/td&gt;
&lt;td&gt;$25/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Squoosh&lt;/td&gt;
&lt;td&gt;✅ Browser-only&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ShortPixel&lt;/td&gt;
&lt;td&gt;❌ Upload&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;100/month&lt;/td&gt;
&lt;td&gt;$3.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compressor.io&lt;/td&gt;
&lt;td&gt;❌ Upload&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;$50/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ImageOptim&lt;/td&gt;
&lt;td&gt;✅ Local&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Free (Mac)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;If &lt;strong&gt;privacy matters&lt;/strong&gt; (and it should — especially with client work), QuickShrink is the strongest TinyPNG alternative. Your images stay on your device, compression happens in-browser, and it's fast.&lt;/p&gt;

&lt;p&gt;For power users who need codec-level control, Squoosh is excellent but limited to one file at a time.&lt;/p&gt;

&lt;p&gt;What's your go-to image compression tool? Let me know in the comments 👇&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webperf</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
