<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Meta on PHP Boy Scout</title><link>https://phpboyscout.uk/categories/meta/</link><description>Recent content in Meta on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Wed, 26 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/categories/meta/index.xml" rel="self" type="application/rss+xml"/><item><title>My blog was shipping 1.2 MB banners</title><link>https://phpboyscout.uk/my-blog-was-shipping-1-2-mb-banners/</link><pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/my-blog-was-shipping-1-2-mb-banners/</guid><description>&lt;img src="https://phpboyscout.uk/my-blog-was-shipping-1-2-mb-banners/cover-my-blog-was-shipping-1-2-mb-banners.png" alt="Featured image of post My blog was shipping 1.2 MB banners" /&gt;&lt;p&gt;There&amp;rsquo;s never a good time to find out your blog is slow, but while people are actually reading it has to be the worst. One of my posts had just gone up on Hacker News, and I was watching the Cloudflare analytics the way you watch a kettle&amp;hellip; and somewhere in among the traffic numbers was a much less flattering story about page performance. My pages were heavy. Embarrassingly heavy, for a static site with no JavaScript framework in sight.&lt;/p&gt;
&lt;p&gt;The culprit wasn&amp;rsquo;t hard to find, because it was the prettiest thing on the page.&lt;/p&gt;
&lt;h2 id="the-covers-were-the-payload"&gt;The covers were the payload
&lt;/h2&gt;&lt;p&gt;Every post here has a cover image, generated as a nice crisp PNG. And the site was serving those PNGs &lt;em&gt;raw&lt;/em&gt;: full resolution, full weight, straight from the repo to your phone. The one at the top of the post being HN&amp;rsquo;d weighed 1.2 MB. The worst offender in the archive was 7.3 MB. Across the whole blog the covers added up to roughly 163 MB of pixels, most of which were being squeezed into a banner slot a few hundred pixels tall on a mobile screen.&lt;/p&gt;
&lt;p&gt;And the averages in the analytics had hidden it beautifully. 28 KB per request looks perfectly reasonable, right up until you notice that the requests that actually matter, the big visual sat above every headline, are forty times that.&lt;/p&gt;
&lt;h2 id="the-pipeline-that-was-there-all-along"&gt;The pipeline that was there all along
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the part that stung. Hugo, the static site generator this blog runs on, has excellent image processing built in: resize, re-encode, generate a &lt;code&gt;srcset&lt;/code&gt; so browsers pick the right size. And the theme I use supports it. But that support is gated behind a family of &lt;code&gt;imageProcessing&lt;/code&gt; params in the site config&amp;hellip; and I had never set them. No error, no warning. Unconfigured, the theme simply shrugs and serves the original file.&lt;/p&gt;
&lt;p&gt;Which is a &lt;a class="link" href="https://phpboyscout.uk/the-config-key-that-quietly-did-nothing/" &gt;config-default trap I keep walking into&lt;/a&gt;, one tool at a time. The capable pipeline and the do-nothing default produce the exact same build log, both green, both looking pleased with themselves. You only ever spot the difference in a reader&amp;rsquo;s download bar, which is about the last place I think to look.&lt;/p&gt;
&lt;p&gt;The fix was one config block and two small template overrides. The config turns the quality and filter dials:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;imaging&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;78&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;resampleFilter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Lanczos&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And a pair of partials, shadowing the theme&amp;rsquo;s own (the joy of a vendored theme is you can override one file without forking the rest), re-encode every cover to WebP at a ladder of widths (480, 720, 1080, 1440) and emit the &lt;code&gt;srcset&lt;/code&gt;, with a full-width WebP as the fallback &lt;code&gt;src&lt;/code&gt; so a raw PNG is never served again, to anyone, for any reason.&lt;/p&gt;
&lt;h2 id="the-numbers-and-the-wrinkles"&gt;The numbers, and the wrinkles
&lt;/h2&gt;&lt;p&gt;The cover that started all this went from 1,197,306 bytes to 68,154 as a full-width WebP, and about 11.5 KB in the 480-wide rendition a phone actually pulls. Call it 94% lighter on desktop and 99% on mobile, multiplied by every post on the site. My earlier back-of-envelope target had been &amp;ldquo;get each under ~150 KB&amp;rdquo;; the actual result embarrassed the estimate in the right direction for once.&lt;/p&gt;
&lt;p&gt;Two wrinkles earned their own commits. First, the social cards. og:image and twitter:image don&amp;rsquo;t get the WebP treatment, because the unfurlers at LinkedIn and friends are years behind the browsers and will happily choke on a modern format (or, for that matter, on a multi-megabyte PNG). So those get their own resize down to a 1200-pixel JPEG: 97 KB, down from the same 1.2 MB. It&amp;rsquo;s a format old enough to vote, but it&amp;rsquo;s the one thing the bots won&amp;rsquo;t fall over.&lt;/p&gt;
&lt;p&gt;Second, GIFs. These are skipped outright now, and there&amp;rsquo;s a reason. The first full build after the change went off and tried to re-encode the animated terminal recordings embedded in some of the tutorials, and it promptly ate every byte of memory the build machine had. A static site generator running out of memory over a GIF of a command prompt was not a sentence I ever expected to write&amp;hellip; but here we are. &lt;code&gt;gif&lt;/code&gt; is now matched and left well alone.&lt;/p&gt;
&lt;h2 id="what-i-can-and-cant-claim"&gt;What I can and can&amp;rsquo;t claim
&lt;/h2&gt;&lt;p&gt;I banked the Cloudflare baseline before the fix, fully intending to do a smug before-and-after with real-user numbers. Then the launch traffic moved on, life happened, and the &amp;ldquo;after&amp;rdquo; snapshot never got pulled. So the plain version is: the byte deltas above are measured and repeatable, and the real-user improvement is merely &lt;em&gt;obvious&lt;/em&gt; rather than &lt;em&gt;proven&lt;/em&gt;. If I ever re-run the numbers I&amp;rsquo;ll append them; the megabyte that no longer travels to your phone doesn&amp;rsquo;t need a dashboard to vouch for it.&lt;/p&gt;
&lt;p&gt;So there&amp;rsquo;s a certain irony in a blog that bangs on about performance and correctness having shipped seven-megabyte banners for weeks without noticing. The cobbler&amp;rsquo;s children, barefoot as ever. It took the customers turning up at the door all at once before I finally looked down at their feet.&lt;/p&gt;</description></item></channel></rss>