<?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: Siva Bollin</title>
    <description>The latest articles on DEV Community by Siva Bollin (@siva_bollin_a68e3f0c053d5).</description>
    <link>https://dev.to/siva_bollin_a68e3f0c053d5</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%2F4138518%2Fcf9faaef-ece1-4076-bc9c-fdecba3e4077.png</url>
      <title>DEV Community: Siva Bollin</title>
      <link>https://dev.to/siva_bollin_a68e3f0c053d5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siva_bollin_a68e3f0c053d5"/>
    <language>en</language>
    <item>
      <title>Hitting an exact GIF file size in the browser without a slow retry loop</title>
      <dc:creator>Siva Bollin</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:16:13 +0000</pubDate>
      <link>https://dev.to/siva_bollin_a68e3f0c053d5/hitting-an-exact-gif-file-size-in-the-browser-without-a-slow-retry-loop-362p</link>
      <guid>https://dev.to/siva_bollin_a68e3f0c053d5/hitting-an-exact-gif-file-size-in-the-browser-without-a-slow-retry-loop-362p</guid>
      <description>&lt;p&gt;Most GIF compressors give you a quality slider and leave you to guess. That is a bad fit for the usual reason people shrink a GIF: a platform has a hard limit. Discord emoji and Slack uploads come with hard size and frame limits. You do not want "roughly smaller", you want "under N bytes, as good as possible".&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://compressgifonline.com" rel="noopener noreferrer"&gt;a browser-only GIF compressor&lt;/a&gt; around that idea. You enter a target size, and it works out the settings. Everything runs on the user's machine. The encoder is &lt;a href="https://www.lcdf.org/gifsicle/" rel="noopener noreferrer"&gt;gifsicle&lt;/a&gt; compiled to WebAssembly, running in a web worker so the page stays responsive. This post is about the search problem on top of it, because that is where the interesting decisions were.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: encodes are slow, the search space is big
&lt;/h2&gt;

&lt;p&gt;The knobs that change output size are width, frame count, color count, and lossy strength. A naive approach is a binary search on one knob. But each full-size encode in WASM is slow (a couple of seconds on my machine for larger GIFs), so ten iterations is already a bad experience, and one knob is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: try the cheap knobs first, in order of damage
&lt;/h2&gt;

&lt;p&gt;The search goes from least to most visible damage:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lossy compression alone, from gentle to strong.&lt;/li&gt;
&lt;li&gt;Fewer colors (128, then 64, then 32).&lt;/li&gt;
&lt;li&gt;Shrinking the width, and if that would cut the width too far, dropping frames first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before walking each ladder, it encodes once with the strongest settings for that step. If even the strongest setting misses the target, the gentler ones cannot fit either, so the whole ladder is skipped. That one check saves several full-size encodes in the common case where the target is far away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: a half-width probe
&lt;/h2&gt;

&lt;p&gt;For the resize step, I do not want to run slow full-size encodes just to find out that none of them fit. So the first encode is at half the width, which is about four times fewer pixels and much faster. That single probe gives a size at a known width.&lt;/p&gt;

&lt;p&gt;From experience with gifsicle on my own sample GIFs, output size scales roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes ∝ width^1.85 × frames^0.85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those exponents are empirical, fitted on my samples with &lt;code&gt;-O3 --lossy=120 --colors=64&lt;/code&gt;, not a law. They are good enough to turn the probe into a starting guess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;predictWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probeWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;probeBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frameStep&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;frameFactor&lt;/span&gt; &lt;span class="o"&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;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frameStep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.85&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;probeWidth&lt;/span&gt; &lt;span class="o"&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;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetBytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probeBytes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;frameFactor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.85&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;The same probe also answers a cheaper question first: could any full-size setting plausibly fit? If the predicted full-size output, even after halving it for the strongest color and lossy settings, is still over the target, the full-size ladders are skipped entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: a bracketed search that refines the model as it goes
&lt;/h2&gt;

&lt;p&gt;The estimate is only a guess, so it is refined with a short search. It keeps a bracket: the widest width known to fit and the narrowest known to miss. After each encode it fits the exponent again from the last two measurements (a log-log secant step), and falls back to 1.85 if the slope looks implausible. It aims about 2% under the target so the next attempt tends to land inside the budget, and it stops when another encode would gain less than about 3% width.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;slope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.85&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;prev&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;width&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;output&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="nx"&gt;slope&lt;/span&gt; &lt;span class="o"&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;log&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;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;slope&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;slope&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;slope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.85&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&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;pow&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;targetBytes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;output&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;slope&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice this converges in a handful of encodes. I have not benchmarked it against a plain binary search, so I will not claim a specific speedup, only that the starting point is much closer than the midpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dropping frames without making the animation faster
&lt;/h2&gt;

&lt;p&gt;Sometimes shrinking width alone would blur the GIF too much. Dropping frames is the other lever, but a naive "delete every second frame" halves the duration, so the animation plays twice as fast.&lt;/p&gt;

&lt;p&gt;The fix is to add the delay of each removed frame to the frame that is kept. If you keep every other frame, each kept frame stays on screen for its own delay plus the delay of the frame that was dropped after it. The total duration stays close to the original, and the function reports the remaining error so it can be checked in tests.&lt;/p&gt;

&lt;p&gt;Two details bit me here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GIF delays are in hundredths of a second, so every compensated delay has to be rounded. Each kept frame gets the rounded sum of its group, so the error stays small, but it is not zero.&lt;/li&gt;
&lt;li&gt;Browsers play delays of 0 to 10 ms as 100 ms. If you compute the frame rate from the raw numbers, a GIF with many zero delays looks much faster than it plays, and you drop frames you could have kept. The fps estimate treats those delays as 100 ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are limits on how far it goes: it never goes below 6 fps from optional frame dropping, never drops beyond keeping every third frame (unless a platform frame cap forces more), and prefers dropping frames over shrinking below half the original width.&lt;/p&gt;

&lt;p&gt;Platform rules go in before the search. A frame cap forces a minimum frame step, and the Discord emoji preset is bounded to a 128x128 box and about 250 KB, so the candidates respect those limits from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  A WASM gotcha worth knowing
&lt;/h2&gt;

&lt;p&gt;Calling gifsicle's CLI entry point repeatedly in one module instance worked until one call failed. After a failed run its global option state stayed dirty and every later call failed too. The fix is to throw away the cached module whenever an exit code is non-zero and create a fresh one on the next task.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not verified
&lt;/h2&gt;

&lt;p&gt;I have not compared compression ratios or speed against other tools, and the size model comes from my own sample set, so it may be off on unusual GIFs (huge flat-color animations, for example, or screen recordings). If you have a GIF that lands far from its target, I would like to see it.&lt;/p&gt;

&lt;p&gt;The tool is free, and nothing is uploaded: &lt;a href="https://compressgifonline.com" rel="noopener noreferrer"&gt;https://compressgifonline.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webassembly</category>
      <category>performance</category>
    </item>
    <item>
      <title>How to Compress Large GIFs Under 8MB for Discord Without Uploading to Servers</title>
      <dc:creator>Siva Bollin</dc:creator>
      <pubDate>Wed, 23 Sep 2026 02:12:34 +0000</pubDate>
      <link>https://dev.to/siva_bollin_a68e3f0c053d5/how-to-compress-large-gifs-under-8mb-for-discord-without-uploading-to-servers-fhf</link>
      <guid>https://dev.to/siva_bollin_a68e3f0c053d5/how-to-compress-large-gifs-under-8mb-for-discord-without-uploading-to-servers-fhf</guid>
      <description>&lt;p&gt;If you've ever tried to share an animated meme, gameplay highlight, or custom emote in a Discord channel, you've likely hit this frustrating wall:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Your files are too powerful! Max file size is 8.00 MB."&lt;/strong&gt; (or 256 KB for custom server animated emojis).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Typical online GIF compressors ask you to upload your personal clips to remote servers, queue them in line behind dozens of other users, and spit out watermarked or severely downscaled files. If the GIF contains private game captures, team demos, or unreleased assets, uploading them to third-party cloud servers poses real privacy concerns.&lt;/p&gt;

&lt;p&gt;Here is how to compress bloated GIFs down to the exact Discord file size limits directly in your browser—with zero server uploads and zero privacy risks.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why GIFs Get So Bloated
&lt;/h3&gt;

&lt;p&gt;Unlike modern video formats (MP4, WebM, AV1), the GIF standard was invented in 1987. It does not utilize modern temporal compression. Instead, file sizes explode due to three factors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Color Depth&lt;/strong&gt;: Each frame stores a 256-color palette. Complex gradients or photographic scenes inflate palette data quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution &amp;amp; Dimensions&lt;/strong&gt;: Discord chat displays animated emojis at 128×128 pixels, but creators often export them at 512×512 or higher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redundant Inter-frame Data&lt;/strong&gt;: Unoptimized GIFs redraw the entire frame canvas each tick rather than only updating moving pixels.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Step-by-Step: Compressing for Discord in Under 10 Seconds
&lt;/h3&gt;

&lt;p&gt;You can use the free web tool &lt;a href="https://compressgifonline.com" rel="noopener noreferrer"&gt;Compress GIF Online&lt;/a&gt;, designed specifically for privacy-first, client-side media processing:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Open the Tool
&lt;/h4&gt;

&lt;p&gt;Navigate to &lt;a href="https://compressgifonline.com" rel="noopener noreferrer"&gt;Compress GIF Online&lt;/a&gt; (or jump directly to the &lt;a href="https://compressgifonline.com/gif-compressor-for-discord/" rel="noopener noreferrer"&gt;Discord GIF Compressor&lt;/a&gt; dedicated preset).&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Drag &amp;amp; Drop Your GIF
&lt;/h4&gt;

&lt;p&gt;Drop your file into the compressor. Notice that processing starts immediately—&lt;strong&gt;no network progress bar or cloud upload happens&lt;/strong&gt;. All processing runs 100% locally within your browser sandbox.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Choose the Discord Target Mode
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For Custom Emojis&lt;/strong&gt;: Select the &lt;strong&gt;Discord Emoji (&amp;lt;256KB)&lt;/strong&gt; preset. It automatically constrains the file size safely under 250 KB and scales dimensions to 128×128 while preserving transparent edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For Chat Attachments / Memes&lt;/strong&gt;: Select &lt;strong&gt;Target Size&lt;/strong&gt; mode and type in &lt;code&gt;8&lt;/code&gt; MB (or &lt;code&gt;10&lt;/code&gt; MB depending on your Discord Nitro tier). The optimization algorithm automatically balances lossy palette reduction and frame dithering to hit your target size without muddying visual quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  4. Preview &amp;amp; Download
&lt;/h4&gt;

&lt;p&gt;Review the real-time split-screen comparison: inspect transparency, frame rate, and playback fluidity side-by-side. Click &lt;strong&gt;Download&lt;/strong&gt;, and your optimized GIF is ready to drop straight into Discord.&lt;/p&gt;




&lt;h3&gt;
  
  
  Watch the Video Demo
&lt;/h3&gt;

&lt;p&gt;Here is a quick walkthrough showing a large GIF compressed down to Discord requirements in just a few clicks:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/2pHDsgbE4t0" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h3&gt;
  
  
  Why 100% Client-Side Processing Matters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Cloud Uploads&lt;/strong&gt;: Your media never leaves your local computer or device. Ideal for confidential team screenshots, private gameplay, or unreleased assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No File Size Caps or Waiting Queues&lt;/strong&gt;: Traditional web compressors impose strict 15MB or 30MB upload limits because server compute is expensive. Client-side processing uses your machine's own CPU, allowing large source files without queues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant Turnaround&lt;/strong&gt;: No uploading latency, no downloading latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it out next time you need to fit a high-energy GIF into your Discord server: &lt;a href="https://compressgifonline.com" rel="noopener noreferrer"&gt;https://compressgifonline.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
