<?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: Prompt Pin</title>
    <description>The latest articles on DEV Community by Prompt Pin (@prompt_pin).</description>
    <link>https://dev.to/prompt_pin</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%2F4059182%2Fcb629646-f5b7-4dce-9eb7-ff6d3c448e3d.png</url>
      <title>DEV Community: Prompt Pin</title>
      <link>https://dev.to/prompt_pin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prompt_pin"/>
    <language>en</language>
    <item>
      <title>How I cut a 1.1 GB image gallery to 30 MB without touching a single database row</title>
      <dc:creator>Prompt Pin</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:12:15 +0000</pubDate>
      <link>https://dev.to/prompt_pin/how-i-cut-a-11-gb-image-gallery-to-30-mb-without-touching-a-single-database-row-1j59</link>
      <guid>https://dev.to/prompt_pin/how-i-cut-a-11-gb-image-gallery-to-30-mb-without-touching-a-single-database-row-1j59</guid>
      <description>&lt;p&gt;I run a small AI-photo-prompt gallery — a few hundred prompts, each with an example image. It started as a weekend project and grew faster than the infrastructure did. By the time I looked properly, the images directory was &lt;strong&gt;1.1 GB across 2,650 files&lt;/strong&gt;, and a single gallery page was shipping 2–3 MB of images to phones on Indian mobile data.&lt;/p&gt;

&lt;p&gt;This is the writeup of getting that under control, and the three things that surprised me along the way — including one bug that cost me an hour and had nothing to do with images at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement that reframed the problem
&lt;/h2&gt;

&lt;p&gt;My first instinct was "the JPEGs are too big, crank the compression." I was wrong. The actual breakdown:&lt;/p&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;Files&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Avg&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PNG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,541&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;892 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;593 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;1,057&lt;/td&gt;
&lt;td&gt;119 MB&lt;/td&gt;
&lt;td&gt;115 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PNG was &lt;strong&gt;58% of the files but 75% of the bytes.&lt;/strong&gt; Same photos, 5× the weight — because whatever generated these exported PNG by default, and PNG is lossless, designed for logos and screenshots, not photographs.&lt;/p&gt;

&lt;p&gt;The dimensions were fine (typical image ~830×1250). This wasn't oversized images. It was pure &lt;em&gt;format&lt;/em&gt; waste.&lt;/p&gt;

&lt;p&gt;Converting to WebP at quality 82:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;423.7 MB  →  29.8 MB   (93% reduction)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 4 MB PNG became a 126 KB WebP. On the actual prompt pages — the ones that get search traffic — the hero image went from &lt;strong&gt;668 KB to 68 KB (89%)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise #1: don't trust a size metric, look at the pixels
&lt;/h2&gt;

&lt;p&gt;RMSE (root-mean-square error vs the original) flagged two images as "possibly degraded" at q82. Before mass-converting a few hundred &lt;em&gt;irreplaceable&lt;/em&gt; images, I generated 100%-zoom side-by-side crops of the busiest region of each and actually looked.&lt;/p&gt;

&lt;p&gt;They were fine. The high RMSE came from &lt;strong&gt;high-saturation graphic content&lt;/strong&gt; — bold colour patterns inflate the metric without any visible degradation. If I'd trusted the number, I'd have wasted 30% more bytes bumping the quality for no visible gain.&lt;/p&gt;

&lt;p&gt;Lesson: for lossy image work, a perceptual eyeball on the worst cases beats any single scalar metric. q82 turned out to be the sweet spot — indistinguishable from the original, including on smooth gradients where banding shows first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision: &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt;, not replacement
&lt;/h2&gt;

&lt;p&gt;Here's the part I'm most glad I got right. I did &lt;strong&gt;not&lt;/strong&gt; convert PNG→WebP in place, and I did &lt;strong&gt;not&lt;/strong&gt; rewrite any image URLs in the database.&lt;/p&gt;

&lt;p&gt;Instead I generated a sibling file next to each original:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;foo.png        (untouched)
foo.png.webp   (new, generated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And served them with a plain &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; element:&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;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/images/foo.png.webp"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/images/foo.png"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"832"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1248"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this shape matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero database writes.&lt;/strong&gt; The image URLs in my DB (and in the mobile app's API that reads the same DB) never changed. Rewriting hundreds of URLs risked breaking the app's response contract for no benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic fallback.&lt;/strong&gt; Any client that can't do WebP gets the original. No feature detection, no JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback is &lt;code&gt;rm&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;find . -name '*.webp' -delete&lt;/code&gt; and the code no-ops itself, because it only emits &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; when a sibling exists.&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; fixed layout shift (CLS) for free while I was in there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The originals were never modified, renamed, or deleted. The worst case for the whole operation was "wasted disk," not "lost data" — which is the failure mode you want when touching hundreds of files you can't regenerate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise #2: I was double-wrapping every image
&lt;/h2&gt;

&lt;p&gt;My gallery HTML is produced by a shortcode, and that shortcode's output flows through &lt;em&gt;two&lt;/em&gt; filters — the shortcode filter itself, and the general content filter. My guard checked whether the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag was already wrapped... but by the second pass, the &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; wrapper was &lt;em&gt;outside&lt;/em&gt; the tag, so the regex matched the bare &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; again and wrapped it a second time.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;48 &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; elements for 26 images.&lt;/strong&gt; Nested &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; is invalid HTML.&lt;/p&gt;

&lt;p&gt;The fix was a &lt;code&gt;data-&lt;/code&gt; marker stamped on tags the first pass had already handled, so the second pass skips them. Obvious in hindsight; invisible until I counted the tags in the rendered output. Which is the real lesson — &lt;em&gt;count the elements in the actual response&lt;/em&gt;, don't assume your filter ran once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise #3: the bug that wasn't about images at all
&lt;/h2&gt;

&lt;p&gt;This one cost the most time. On the pages I render with a custom document template, the site nav suddenly rendered &lt;strong&gt;centred instead of right-aligned&lt;/strong&gt; — but &lt;em&gt;only&lt;/em&gt; on those pages.&lt;/p&gt;

&lt;p&gt;I diffed the markup against a working page. Byte-identical. Same container classes, same &lt;code&gt;is-content-justification-right&lt;/code&gt;. The HTML was correct.&lt;/p&gt;

&lt;p&gt;What was missing was the &lt;strong&gt;CSS defining those classes.&lt;/strong&gt; A working page emitted 7 layout rules; my custom page emitted &lt;strong&gt;zero&lt;/strong&gt;. The specific casualty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.wp-container-&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="nc"&gt;.-is-layout-46c2b0f2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex-end&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 cause: this CSS framework collects block layout styles &lt;em&gt;while blocks render&lt;/em&gt;, then flushes them into &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. My template called the head-flush &lt;strong&gt;first&lt;/strong&gt; and rendered the header &lt;strong&gt;afterwards&lt;/strong&gt; — so by the time the header produced its container classes, the stylesheet had already been sent. The classes existed in the DOM with nothing defining them.&lt;/p&gt;

&lt;p&gt;Fix: render the header and footer into buffers &lt;em&gt;before&lt;/em&gt; flushing the head, then echo them. Styles get registered in time.&lt;/p&gt;

&lt;p&gt;The general lesson has stuck with me: when markup is identical but rendering differs, &lt;strong&gt;stop looking at the markup.&lt;/strong&gt; The difference is somewhere in the pipeline — order of operations, a style registry, a hook that didn't fire — not in the HTML you keep re-reading.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Image payload on the pages that matter: &lt;strong&gt;~90% smaller&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No database changes, so the companion mobile app was completely unaffected&lt;/li&gt;
&lt;li&gt;Fully reversible at every layer&lt;/li&gt;
&lt;li&gt;The originals sit untouched on disk, plus a verified full backup, because you do not mass-process irreplaceable files without a tested restore path&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you're doing something similar
&lt;/h2&gt;

&lt;p&gt;A few things I'd tell myself at the start:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure format distribution before compression settings.&lt;/strong&gt; My whole win was "PNG photos are the problem," not "quality is too high."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert alongside, serve with &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt;, never rewrite URLs.&lt;/strong&gt; It makes the whole thing reversible and keeps any downstream API consumers oblivious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eyeball the worst cases at 100%.&lt;/strong&gt; One perceptual check beats a directory of RMSE numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count elements in the rendered output.&lt;/strong&gt; Both my real bugs — double-wrapping and missing CSS — were invisible in the source and obvious the moment I counted what actually shipped.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;The gallery this came from is &lt;a href="https://promptpin.in/ai-photo-prompts-for-girls/" rel="noopener noreferrer"&gt;PromptPin&lt;/a&gt; — a searchable set of copy-paste AI photo prompts, each shown with its example image. Built on WordPress with a separate prompts database and a custom gallery plugin, which is a whole other writeup. Happy to go deeper on any of the above in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>webdev</category>
      <category>php</category>
      <category>wordpress</category>
    </item>
  </channel>
</rss>
