<?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: NOGUCHILin</title>
    <description>The latest articles on DEV Community by NOGUCHILin (@noguchilin).</description>
    <link>https://dev.to/noguchilin</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%2F4008732%2Fdbccd4a5-b4ab-499f-b2cf-c38da232e5dc.png</url>
      <title>DEV Community: NOGUCHILin</title>
      <link>https://dev.to/noguchilin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/noguchilin"/>
    <language>en</language>
    <item>
      <title>Your &lt;link rel=preload&gt; is downloading the file twice</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:57:56 +0000</pubDate>
      <link>https://dev.to/noguchilin/your-is-downloading-the-file-twice-5d6g</link>
      <guid>https://dev.to/noguchilin/your-is-downloading-the-file-twice-5d6g</guid>
      <description>&lt;p&gt;Here's a bug that hides in plain sight: a preload hint that makes your site &lt;em&gt;slower&lt;/em&gt;, in a way that looks perfectly correct in the HTML.&lt;/p&gt;

&lt;p&gt;I found it last week while auditing a WordPress site before a host migration. The homepage was shipping 2.88 MB across 141 requests, and I was going through the waterfall looking for the usual suspects. Then I noticed the same filename twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two lines
&lt;/h2&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;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/wp-content/themes/x/library/js/plugins.min.js"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"script"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
...
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/wp-content/themes/x/library/js/plugins.min.js?ver=2.11.5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at them for a second. Same path. Same file. The only difference is &lt;code&gt;?ver=2.11.5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That difference is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the browser doesn't care that it's "the same file"
&lt;/h2&gt;

&lt;p&gt;The preload cache is keyed by the &lt;strong&gt;full request URL&lt;/strong&gt;, query string included. It is not keyed by path, and it is certainly not keyed by content.&lt;/p&gt;

&lt;p&gt;So the browser does exactly what you told it to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sees the preload, fetches &lt;code&gt;plugins.min.js&lt;/code&gt; at high priority.&lt;/li&gt;
&lt;li&gt;Parses on, hits the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, and looks for &lt;code&gt;plugins.min.js?ver=2.11.5&lt;/code&gt; in the preload cache.&lt;/li&gt;
&lt;li&gt;Doesn't find it — different URL — and fetches the whole thing again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The preloaded copy is never used. It sits there and expires.&lt;/p&gt;

&lt;p&gt;I verified the two responses were genuinely identical rather than assuming it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;".../plugins.min.js?ver=2.11.5"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; a.js
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;".../plugins.min.js"&lt;/span&gt;            &lt;span class="nt"&gt;-o&lt;/span&gt; b.js
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 a.js b.js
&lt;span class="go"&gt;94b4a2673e7c30d5b3dac15a9b55613ee106df3e91b0c65d052f94033ea7de74  a.js
94b4a2673e7c30d5b3dac15a9b55613ee106df3e91b0c65d052f94033ea7de74  b.js
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Byte-for-byte the same file. Downloaded twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is worse than a wasted request
&lt;/h2&gt;

&lt;p&gt;If it were merely wasteful, I wouldn't have bothered writing about it. The problem is &lt;em&gt;when&lt;/em&gt; the waste happens.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rel=preload&lt;/code&gt; exists to say "this is important, get it now." The browser honours that: it fetches at high priority, immediately, during the exact window when the page is trying to paint. So the wasted copy isn't downloading during idle time at the end. It's competing for bandwidth with your LCP resource, at the front of the queue, by explicit request.&lt;/p&gt;

&lt;p&gt;On the site I was looking at, both of the page's script preloads had this mismatch:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Transferred (gzip)&lt;/th&gt;
&lt;th&gt;Downloaded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;plugins.min.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;248 KB&lt;/td&gt;
&lt;td&gt;twice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;init.min.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;28 KB&lt;/td&gt;
&lt;td&gt;twice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;~276 KB of high-priority bandwidth, spent on bytes that were thrown away.&lt;/strong&gt; On a mobile connection during the critical rendering window.&lt;/p&gt;

&lt;p&gt;And the LCP element on that page? A CSS background image — which the preload scanner can't see at all, so it gets discovered late and queued &lt;em&gt;behind&lt;/em&gt; all this. The one thing that actually needed the bandwidth was the one thing not getting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WordPress produces this so reliably
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wp_enqueue_script()&lt;/code&gt; appends a cache-busting &lt;code&gt;?ver=&lt;/code&gt; to script URLs. That's standard, sensible behaviour.&lt;/p&gt;

&lt;p&gt;The preload hints, though, usually come from somewhere else — a theme's performance option, or an optimization plugin generating resource hints. That code often builds the URL from the file path and forgets that the enqueue system is going to add a version string to the tag it's pairing with.&lt;/p&gt;

&lt;p&gt;Both halves are individually reasonable. Together they silently double-fetch.&lt;/p&gt;

&lt;p&gt;This is not a WordPress-only failure, to be clear. Any setup where preload hints and asset URLs are generated by different layers can drift: asset pipelines with content hashes, CDN rewrites, cache-busting middleware. WordPress just makes it especially easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding it in your own site
&lt;/h2&gt;

&lt;p&gt;Don't rely on spotting it by eye — the two URLs are long and differ by a few characters near the end. Paste this in the console instead:&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;preloads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link[rel=preload][as=script]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&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;scripts&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script[src]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;preloads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;scripts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// exact match, fine&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;same&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;same&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`MISMATCH\n  preload: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n  script : &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;same&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`UNUSED preload: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;&lt;code&gt;MISMATCH&lt;/code&gt; means you're double-downloading. &lt;code&gt;UNUSED&lt;/code&gt; means you're preloading something the page never requests at all — also worth deleting.&lt;/p&gt;

&lt;p&gt;That's how I confirmed it on the site above: two preloads, both &lt;code&gt;MISMATCH&lt;/code&gt;. Chrome may also surface a "preloaded using link preload but not used within a few seconds" warning in the Issues panel, but I wouldn't wait to notice it — run the check.&lt;/p&gt;

&lt;p&gt;One caveat before you trust a clean result. That snippet reads the DOM, and on a page served by an optimization plugin the DOM is the &lt;em&gt;optimizer's output&lt;/em&gt;, not what your server generated. I have watched the same URL hand back two different documents depending on whether it came from cache — in one case an entire inline &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block was full on the origin and empty in the cached copy. So if the check comes back clean on a heavily cached site, confirm against the raw HTML before you believe it:&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;url&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?nocache=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;html&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;link&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*rel=&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]?&lt;/span&gt;&lt;span class="sr"&gt;preload&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]?[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;/gi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; preload tags at origin&lt;/span&gt;&lt;span class="dl"&gt;'&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link[rel=preload]&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; preload tags in the DOM&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;If those two numbers disagree, audit the origin copy — that's the one your server is actually producing.&lt;/p&gt;

&lt;p&gt;Same idea applies to &lt;code&gt;as="style"&lt;/code&gt;, &lt;code&gt;as="font"&lt;/code&gt;, and &lt;code&gt;as="image"&lt;/code&gt;. Fonts are the nastiest of the three, because a mismatched font preload also fails to warm the cache before the font is needed, so you pay twice &lt;em&gt;and&lt;/em&gt; still get the swap.&lt;/p&gt;

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

&lt;p&gt;Make the two URLs identical. Either add the same &lt;code&gt;?ver=&lt;/code&gt; to the preload href, or drop the version from the script tag — whichever your setup lets you control.&lt;/p&gt;

&lt;p&gt;If you can't easily reach the code generating the hint, &lt;strong&gt;just delete the preload.&lt;/strong&gt; A missing preload costs you a little discovery latency. A mismatched one costs you the full file size at the worst possible moment. Deleting is strictly better than leaving it broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;A preload tag is a promise that the browser will keep literally. It doesn't check whether you meant the same file. It checks whether you typed the same URL.&lt;/p&gt;

&lt;p&gt;So when you audit resource hints, don't read them for intent. Diff them against what the page actually requests. Two URLs that a human would call "the same file" are, to the browser, simply two files.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I do this kind of technical detective work constantly. If you're tired of guessing why your site is slow, I packaged my exact audit checklist into &lt;a href="https://noguchilin.gumroad.com/l/ywtkug?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=your-is-downloading-the-file-twice" rel="noopener noreferrer"&gt;SpeedKit for Claude Code&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>webperf</category>
    </item>
    <item>
      <title>Hidden in 26,695 characters of Additional CSS: the rule that broke every mobile header</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:54:57 +0000</pubDate>
      <link>https://dev.to/noguchilin/hidden-in-26695-characters-of-additional-css-the-rule-that-broke-every-mobile-header-2jeg</link>
      <guid>https://dev.to/noguchilin/hidden-in-26695-characters-of-additional-css-the-rule-that-broke-every-mobile-header-2jeg</guid>
      <description>&lt;p&gt;Sometimes a performance problem isn't a lack of optimization. Sometimes it's a ghost that has been haunting your stylesheet for years.&lt;/p&gt;

&lt;p&gt;I was recently brought in to diagnose a persistent Core Web Vitals issue on a WordPress site. Largest Contentful Paint was sitting between 5.4 and 5.8 seconds on throttled mobile and would not move. The previous developers had tried the usual things: optimizing images, adding preload tags, tuning caching. None of it helped.&lt;/p&gt;

&lt;p&gt;When I looked at the LCP breakdown, the timing of the paint event didn't match the download of the header image we were &lt;em&gt;supposed&lt;/em&gt; to be seeing. It matched the completion time of a completely different image file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The investigation
&lt;/h2&gt;

&lt;p&gt;The HTML was correct. The &lt;code&gt;&amp;lt;link rel="preload"&amp;gt;&lt;/code&gt; tags were well formed and pointed at the right optimized image. The network panel showed that image downloading quickly.&lt;/p&gt;

&lt;p&gt;But on mobile, the site wasn't displaying it.&lt;/p&gt;

&lt;p&gt;I started stripping away layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disabled custom plugins? Still there.&lt;/li&gt;
&lt;li&gt;Checked the theme's page builder settings? Correct image selected.&lt;/li&gt;
&lt;li&gt;Looked for rogue JavaScript swapping the &lt;code&gt;src&lt;/code&gt;? None found.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally I looked at the one place you hope you never have to: &lt;code&gt;Appearance &amp;gt; Customize &amp;gt; Additional CSS&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  26,695 characters of "quick tweaks"
&lt;/h2&gt;

&lt;p&gt;In WordPress, the Customizer has an "Additional CSS" field. It is meant for small adjustments. On this site it had grown, over years, into a single block of 26,695 characters. (That is measured, not estimated — and getting that number turned out to be its own trap, which I'll show you at the end.)&lt;/p&gt;

&lt;p&gt;Buried in it was a rule shaped like this:&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;.touch&lt;/span&gt; &lt;span class="nf"&gt;#page-header&lt;/span&gt; &lt;span class="nc"&gt;.row-background.background-element&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url(https://old-staging-host.example/wp-content/uploads/header-photo.jpg)&lt;/span&gt; &lt;span class="cp"&gt;!important&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;Four things made it lethal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.touch&lt;/code&gt; — it only fired on touch devices, so nobody testing on a desktop would ever see it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#page-header&lt;/code&gt; — it targeted an ID used on &lt;em&gt;every single page&lt;/em&gt; of the site.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;background-image&lt;/code&gt; — a CSS background is invisible to the browser's preload scanner, so it is discovered late.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;!important&lt;/code&gt; — no theme setting or optimization plugin could override it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the URL pointed at an old, abandoned staging server that nobody had thought about in years.&lt;/p&gt;

&lt;p&gt;Because this CSS was injected globally, &lt;strong&gt;every mobile visitor to every page was rendering the wrong header image&lt;/strong&gt; — one that lived on someone else's server and had never been optimized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it defeated every previous attempt
&lt;/h2&gt;

&lt;p&gt;This one rule explained the whole mystery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The preload tags "didn't work" because they preloaded the correct image, while the CSS made the browser fetch and paint a different, heavier one.&lt;/li&gt;
&lt;li&gt;Image optimization plugins couldn't touch it, because the image wasn't on this site at all.&lt;/li&gt;
&lt;li&gt;It was invisible on desktop by construction: the selector starts with &lt;code&gt;.touch&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is worth sitting with. The bug was written in a way that guarantees the person checking their work will not see it.&lt;/p&gt;

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

&lt;p&gt;I deleted the rule.&lt;/p&gt;

&lt;p&gt;LCP went from a 5.4–5.8s baseline to 3.6–4.3s, across the whole site, because Additional CSS is global — removing it fixed every page at once. That is a floor of about 1.5 seconds and a ceiling of about 2.2, depending on which end of each range you compare. I'm giving you both ends rather than the flattering pair, because single runs on a page like this scatter, and one cherry-picked before/after is how people end up believing things that aren't true.&lt;/p&gt;

&lt;p&gt;No new plugin. No new optimization. One deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own — and the trap that will tell you there's nothing there
&lt;/h2&gt;

&lt;p&gt;WordPress prints the Additional CSS field inline in your page source, in a &lt;code&gt;&amp;lt;style id="wp-custom-css"&amp;gt;&lt;/code&gt; block. So you can measure it from outside, without logging in.&lt;/p&gt;

&lt;p&gt;But if you do the obvious thing — read it out of the DOM — you may get a very reassuring, very wrong answer:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#wp-custom-css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&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="c1"&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero. The element is right there and it is empty.&lt;/p&gt;

&lt;p&gt;That is what I got on this site. The Additional CSS had not gone anywhere; a caching plugin's "remove unused CSS" feature had rewritten the cached page, emptied that block, and folded the rules it decided to keep into a generated stylesheet of its own. Reading the live DOM tells you about the optimizer's output, not about what is actually stored in your site.&lt;/p&gt;

&lt;p&gt;Fetch the un-cached page instead:&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;// paste in the browser console on your own site&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?nocache=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;html&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;css&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;style&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*id=&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;wp-custom-css&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;][^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;([\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;style&amp;gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&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;span class="mi"&gt;1&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="nx"&gt;css&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; characters of Additional CSS&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/!important/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; !important&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/[^&lt;/span&gt;&lt;span class="sr"&gt;)'" &lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;)))]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Side by side on the same page, same minute, that was the difference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;HTML size&lt;/th&gt;
&lt;th&gt;Additional CSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Normal request (served from cache)&lt;/td&gt;
&lt;td&gt;576,036 chars&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same URL with a cache-buster&lt;/td&gt;
&lt;td&gt;177,012 chars&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26,695&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last line of the snippet is the one that matters. It lists every external URL your "quick tweaks" still point at. If a staging host, an old domain, or a former agency's server shows up there, you have found the same class of bug.&lt;/p&gt;

&lt;p&gt;And notice what the first table row means: the tool installed to make the site fast is also the reason nobody found this for years. It hid the junk drawer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;When your optimizations aren't moving the needle, stop adding more optimizations.&lt;/p&gt;

&lt;p&gt;Look at what actually painted. If the element rendering isn't the element you optimized, your problem isn't speed — it's that something is overriding you, and you haven't found it yet.&lt;/p&gt;

&lt;p&gt;And check the &lt;code&gt;Additional CSS&lt;/code&gt; box. It's the junk drawer of WordPress, and it does not show up in any audit tool's checklist.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I do this kind of technical detective work constantly. If you're tired of guessing why your site is slow, I packaged my exact audit checklist into &lt;a href="https://noguchilin.gumroad.com/l/ywtkug?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=hidden-in-26695-characters-of-additional-css" rel="noopener noreferrer"&gt;SpeedKit for Claude Code&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>wordpress</category>
      <category>performance</category>
    </item>
    <item>
      <title>WooCommerce won't filter by two attributes at once. A client needed 40 category pages that do exactly that.</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Fri, 24 Jul 2026 00:33:38 +0000</pubDate>
      <link>https://dev.to/noguchilin/woocommerce-wont-filter-by-two-attributes-at-once-a-client-needed-40-category-pages-that-do-1829</link>
      <guid>https://dev.to/noguchilin/woocommerce-wont-filter-by-two-attributes-at-once-a-client-needed-40-category-pages-that-do-1829</guid>
      <description>&lt;p&gt;This week's job: an ecommerce store selling made-to-size products wanted 40 SEO landing pages - one per popular size. Think "12 x 24 Widget Frames" as a page that ranks, instead of a filter URL that never will.&lt;/p&gt;

&lt;p&gt;The catch is small but real, and if you've tried this you've hit it: &lt;strong&gt;WooCommerce's product listing tools take one attribute filter, not two.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, concretely
&lt;/h2&gt;

&lt;p&gt;The store models size as two product attributes: &lt;code&gt;pa_width&lt;/code&gt; and &lt;code&gt;pa_height&lt;/code&gt;. Their menu links to filtered listings like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/product-category/frames/?filter-width=341&amp;amp;filter-height=321
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems with parameter URLs like that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They don't rank.&lt;/strong&gt; Google mostly treats them as duplicates of the base category. Forty of these links means forty pages of crawl noise and zero landing pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't replace them with the stock shortcode.&lt;/strong&gt; &lt;code&gt;[products attribute="width" terms="341"]&lt;/code&gt; works - but there's no second attribute slot. Width AND height together isn't a thing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The site had ~350 pages, Elementor everywhere, and a live store I wasn't allowed to break (their words: "please keep in mind that this is a live site"). So: no new plugins, no theme surgery, nothing clever. Small, boring, reversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: 30 lines and one filter hook
&lt;/h2&gt;

&lt;p&gt;WooCommerce's &lt;code&gt;[products]&lt;/code&gt; shortcode builds a WP_Query and - helpfully - passes the query args through a filter before running it. So you wrap it: accept two term IDs, inject both as a &lt;code&gt;tax_query&lt;/code&gt;, render, clean up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// [size_products width="341" height="321"]&lt;/span&gt;
&lt;span class="nf"&gt;add_shortcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'size_products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$atts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shortcode_atts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'width'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'height'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'columns'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'limit'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'-1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'size_products'&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="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'width'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'height'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;class_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WC_Shortcode_Products'&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$query_args&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query_args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'tax_query'&lt;/span&gt;&lt;span class="p"&gt;][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pa_width'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'field'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'terms'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'width'&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;span class="nv"&gt;$query_args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'tax_query'&lt;/span&gt;&lt;span class="p"&gt;][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pa_height'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'field'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'terms'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'height'&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query_args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'woocommerce_shortcode_products_query'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$shortcode&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;WC_Shortcode_Products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'columns'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'columns'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'limit'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'orderby'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'menu_order title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'order'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ASC'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$shortcode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_content&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;remove_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'woocommerce_shortcode_products_query'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$html&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;Notes that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It renders your theme's normal product cards.&lt;/strong&gt; Because it goes through &lt;code&gt;WC_Shortcode_Products&lt;/code&gt;, the output is the standard WooCommerce loop - same markup your theme already styles. The pages look native because they are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching is free.&lt;/strong&gt; The shortcode caches per-args, and since width/height are part of the args, every size page gets its own cache entry. No stampede.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add and remove the filter around a single render.&lt;/strong&gt; Leave it attached and you'll quietly contaminate every other &lt;code&gt;[products]&lt;/code&gt; call on the page.&lt;/li&gt;
&lt;li&gt;Drop it in via your snippets plugin or child theme. On a live site I prefer a snippets plugin: one toggle to revert.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that actually took thinking: don't recategorize 350 products
&lt;/h2&gt;

&lt;p&gt;The obvious alternative was "just create a size category and assign products to it." That means editing every product, duplicating information the attributes already hold, and a second source of truth that drifts. The attribute-based query means &lt;strong&gt;product data stays exactly as the store manages it today&lt;/strong&gt; - new products with the right width/height attributes appear on the right size page automatically.&lt;/p&gt;

&lt;p&gt;One prep step made the rollout mechanical: before building anything, I extracted the full attribute term map from the site's own menu markup (15 widths x 15 heights, each label already paired with its term ID in the filter URLs). Forty pages then reduced to a lookup table: page title, slug, two term IDs, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SEO wrapper around it
&lt;/h2&gt;

&lt;p&gt;Each size page is a normal page (cloned from the client's existing landing-page template, so design review took minutes, not days):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;H1 and title tag with the size phrase people actually search&lt;/li&gt;
&lt;li&gt;a unique intro paragraph and meta description per size&lt;/li&gt;
&lt;li&gt;clean slug like &lt;code&gt;/12-x-24-widget-frames/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the shortcode with that size's two term IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old parameter URLs keep working - nothing redirects, nothing breaks. The menu just points at real pages now.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest footnote
&lt;/h2&gt;

&lt;p&gt;The grid shows exactly what's in the catalog - which is how we found that one size was missing a variant everyone assumed existed (its mirrored orientation existed; the portrait one didn't). A faithful query is also a free data audit. Report those findings to the store owner instead of papering over them; it's the most trust you can buy with one message.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I measure before I touch anything - it's how I work through &lt;a href="https://www.upwork.com/freelancers/~01f8a75c6d7a4b47e5" rel="noopener noreferrer"&gt;Upwork&lt;/a&gt; and why my write-ups have numbers in them. Previous entries in this series: &lt;a href="https://dev.to/noguchilin/the-biggest-fix-for-this-slow-woocommerce-checkout-was-removing-an-optimization-plugin-60-472a"&gt;cutting a WooCommerce checkout by 60% by removing an optimization plugin&lt;/a&gt; and &lt;a href="https://dev.to/noguchilin/is-your-wordpress-host-the-real-bottleneck-how-to-tell-in-10-minutes-with-numbers-5daj"&gt;how to tell if your WordPress host is the real bottleneck&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>woocommerce</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The same number, two different diseases: 5 audit signals that mean something else than you think</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Thu, 23 Jul 2026 00:19:05 +0000</pubDate>
      <link>https://dev.to/noguchilin/the-same-number-two-different-diseases-5-audit-signals-that-mean-something-else-than-you-think-15ej</link>
      <guid>https://dev.to/noguchilin/the-same-number-two-different-diseases-5-audit-signals-that-mean-something-else-than-you-think-15ej</guid>
      <description>&lt;p&gt;Last week I wrote about &lt;a href="https://dev.to/noguchilin/i-ran-technical-audits-on-8-sites-that-asked-reddit-for-feedback-the-same-5-things-kept-breaking-31nc"&gt;5 things that kept breaking across 8 sites&lt;/a&gt; — failures that score fine and still hurt, because no measurement tool ever surfaces them. In the comments, &lt;a class="mentioned-user" href="https://dev.to/fromzerotoship"&gt;@fromzerotoship&lt;/a&gt; handed me the mirror image from the infra side: signals that &lt;em&gt;do&lt;/em&gt; show up, loudly, but mean something completely different than they say. A firewall rewriting legitimate 4xx responses into 405s. A 404 that means "not deployed yet," not "wrong path."&lt;/p&gt;

&lt;p&gt;He named the split better than I could: a tool can fail you by &lt;strong&gt;staying silent&lt;/strong&gt; about a real problem, or by &lt;strong&gt;reporting a real signal that means something else here&lt;/strong&gt;. The silent kind wastes your time once. The lying kind teaches you to distrust the whole layer.&lt;/p&gt;

&lt;p&gt;So here's the follow-up he suggested: the five signals I keep meeting in web audits where the number is real, but the diagnosis it implies is wrong. For each one — what it says, what it can actually mean, and the fastest check that tells you which disease you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A 200 that means "page not found"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The signal:&lt;/strong&gt; every URL returns 200. Crawler happy, uptime monitor green, audit tool scores it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it can actually mean:&lt;/strong&gt; the theme's "nothing found" template rendered beautifully — with a 200 status. Soft 404s are invisible to almost every automated check because the page &lt;em&gt;is&lt;/em&gt; a valid page. It just isn't the one anyone wanted. Search engines eventually notice (Search Console has a dedicated bucket for this), but your monitoring never will.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second check:&lt;/strong&gt; don't trust the status line — trust the content. &lt;code&gt;curl -s &amp;lt;url&amp;gt; | grep -ci "nothing found\|no results\|not exist"&lt;/code&gt;. If you have a sitemap, spot-check the thinnest pages in it: same boilerplate word count on ten "different" URLs is the tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A Lighthouse "regression" that means "you hit a cold cache"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The signal:&lt;/strong&gt; yesterday's run: 78. Today's run, after your fix: 61. Conclusion: the fix made it worse. Revert?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it can actually mean:&lt;/strong&gt; run one hit a page-cache HIT, run two hit a MISS. On a WordPress site I traced this week, the same document arrived in ~320ms warm and 2.4s cold — that single difference reshuffles every downstream phase (FCP, LCP, even which element &lt;em&gt;is&lt;/em&gt; the LCP). Neither run is lying. They're measuring two different servers that happen to share a hostname.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second check:&lt;/strong&gt; look at the TTFB of the runs you're comparing before you compare anything else. If they differ by an order of magnitude, you're not A/B-testing your fix — you're A/B-testing the cache. Warm the URL once, then measure three runs and read the median. (And check response headers: most page caches announce HIT/MISS.)&lt;/p&gt;

&lt;h2&gt;
  
  
  3. "Element Render Delay" — one number, three unrelated fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The signal:&lt;/strong&gt; Lighthouse says your LCP breakdown is dominated by render delay, 700-1700ms. The image is small and loads fast, so... the image isn't the problem. Now what?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it can actually mean:&lt;/strong&gt; at least three completely different things, each with a different owner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;intro animation&lt;/strong&gt; on the LCP element or an ancestor — the browser won't count the element painted until it reaches its final visible state, so a 0.8s hero fade &lt;em&gt;is&lt;/em&gt; 800ms of render delay;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;render-blocking CSS or fonts&lt;/strong&gt; — the element is ready, the render tree isn't;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;layout waiting on JavaScript&lt;/strong&gt; — the element's size is computed at runtime (a height-ratio the theme resolves in JS), so paint can't happen until that script runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same number. Fix one is a CSS change, fix two is resource loading, fix three is theme architecture. Guess wrong and you'll "optimize" for a week with nothing to show — which is usually the story behind "we tried everything and the score didn't move."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second check:&lt;/strong&gt; DevTools Performance panel, mobile throttling, one recording. Click the LCP marker and read what actually sits inside the render-delay span: an animation frame, a blocking stylesheet, or a long task. It's written right there; almost nobody looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. "The image is tiny and fast" — but the image was never the problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The signal:&lt;/strong&gt; LCP element is an image, it's 29KB, it downloads in milliseconds. So LCP should be fine. It's 6 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it can actually mean:&lt;/strong&gt; the "image" is a &lt;strong&gt;CSS background&lt;/strong&gt;, not an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;. The preload scanner can't see it — the request can't even &lt;em&gt;start&lt;/em&gt; until the CSS that references it is parsed. On the site from example 2, that meant chewing through a 384KB inline style block (80% of the HTML document) before the browser learned the image existed. Load time: fast. &lt;em&gt;Discovery&lt;/em&gt; time: 2.75 seconds. The weight of the file was never in the equation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second check:&lt;/strong&gt; in DevTools, hover the LCP entry and see what element it points to. If it's a &lt;code&gt;div&lt;/code&gt; with &lt;code&gt;background-image&lt;/code&gt;, stop optimizing the file and start optimizing its discoverability — a per-page preload, or promote it to a real &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;. Also worth 30 seconds: check what your optimizer plugin is &lt;em&gt;already&lt;/em&gt; preloading. I found one pushing &lt;code&gt;fetchpriority=high&lt;/code&gt; on an image that doesn't appear on the page at all — a preload actively working against the element that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A timeout that means "wrong network layer entirely"
&lt;/h2&gt;

&lt;p&gt;This one is &lt;a class="mentioned-user" href="https://dev.to/fromzerotoship"&gt;@fromzerotoship&lt;/a&gt;'s, and I'm including it because it's the purest case of the pattern: an external API call times out with an error that looks exactly like "the API is down." The API is fine. Outbound IPv6 is silently blocked, and the fix is one flag forcing IPv4. Every retry, every backoff strategy, every status-page check is aimed at the wrong layer — because the error message names the wrong suspect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second check:&lt;/strong&gt; when a remote service "is down" but its status page disagrees, test the same call from a different network path (another box, mobile hotspot, &lt;code&gt;curl -4&lt;/code&gt; vs &lt;code&gt;curl -6&lt;/code&gt;) before touching your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;When we compared lists in the comments, &lt;a class="mentioned-user" href="https://dev.to/fromzerotoship"&gt;@fromzerotoship&lt;/a&gt; pointed out something I'd missed about my own examples: a single metric can fork on three different axes. It can fork on &lt;strong&gt;state&lt;/strong&gt; — the 200 that's really an empty page (success vs. empty, same code). It can fork on &lt;strong&gt;condition&lt;/strong&gt; — the Lighthouse score that moved because of cache temperature, not your fix (cold vs. warm, same page). And it can fork on &lt;strong&gt;cause&lt;/strong&gt; — the render-delay band that's an animation on one site, a font on the next, JS layout on the third (three fixes, same number). So the sixty-second question isn't just "is this signal lying" — it's "&lt;strong&gt;which axis did it fork on?&lt;/strong&gt;" State, condition, or cause. Answer that first and the fix usually names itself.&lt;/p&gt;

&lt;p&gt;All five have the same shape: &lt;strong&gt;the signal is real, the implied suspect is wrong.&lt;/strong&gt; The number survives being written down, travels into a ticket, and the ticket sends someone to fix the wrong thing — that's what makes this class more expensive than silent failures. A silent failure costs you the time until you notice. A misdirecting signal costs you the time you spend confidently fixing something that was never broken.&lt;/p&gt;

&lt;p&gt;The defense is boring and works: before acting on any single metric, spend sixty seconds finding out &lt;em&gt;which&lt;/em&gt; of its meanings you're holding. The checks above are all under a minute. The weeks I've watched people spend on the wrong fix were not.&lt;/p&gt;

&lt;p&gt;What's the signal that lied to you the longest? The comment section on the last post turned into the best checklist I didn't write — happy to be outdone again.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>debugging</category>
      <category>seo</category>
    </item>
    <item>
      <title>I ran technical audits on 8 sites that asked Reddit for feedback. The same 5 things kept breaking.</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Tue, 21 Jul 2026 04:35:45 +0000</pubDate>
      <link>https://dev.to/noguchilin/i-ran-technical-audits-on-8-sites-that-asked-reddit-for-feedback-the-same-5-things-kept-breaking-31nc</link>
      <guid>https://dev.to/noguchilin/i-ran-technical-audits-on-8-sites-that-asked-reddit-for-feedback-the-same-5-things-kept-breaking-31nc</guid>
      <description>&lt;p&gt;Over the past week I've been doing free technical audits for sites posted in feedback subreddits (r/websitefeedback, r/reviewmyshopify and similar) - measuring each one with curl, Chrome DevTools and raw HTML reads before commenting. Eight audits in, the same handful of problems keeps showing up, and they're mostly NOT the things speed tools yell about. Here they are, with real numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Your share preview is your storefront window - and it's often empty
&lt;/h2&gt;

&lt;p&gt;A merch brand about to promote its store to an engaged social audience had no og:image, no twitter:image, and no meta description. Every link shared to Instagram Stories, X, Discord or iMessage unfurled as a text-only card with a blank thumbnail - for a brand whose entire pitch is aesthetics. Same store's homepage title tag was the bare domain (the platform fallback when the SEO title is unset).&lt;/p&gt;

&lt;p&gt;The fix is minutes: one strong social sharing image + a homepage meta description, set before the promo push, not after. If you check nothing else before launch, check what your link looks like when pasted into a chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Entrance animations hide your page from the people you're trying to impress
&lt;/h2&gt;

&lt;p&gt;An education platform's hero - headline, guarantee box, CTA - sat at opacity 0 for over six seconds on my first cold load, while entrance animations waited for ~10MB of eager-loaded media. A coaching site had the same pattern: full navy screen, zero words, until a fade-in ran. Both sites look great in the founder's warm-cached browser. First-time visitors on a school Chromebook or hotel wifi see a blank panel - and that's exactly the audience being courted.&lt;/p&gt;

&lt;p&gt;If content is invisible until JavaScript says otherwise, your worst-case first impression is nothing at all. Fade things in fast, or don't gate the hero on animation at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Photos exported as PNG are quietly most of your page weight
&lt;/h2&gt;

&lt;p&gt;One founder photo: 640x720 pixels, exported as PNG with alpha, 783KB - referenced three times on the page. Saved as JPG or WebP it's ~50KB, a 15x cut on a single file with zero visible change. The same site's lifestyle photos were all 200-260KB PNGs; the page shipped ~4MB of images with not a single loading="lazy".&lt;/p&gt;

&lt;p&gt;Rule of thumb that would have fixed every case I saw: photos are never PNG. PNG is for screenshots, flat graphics and things that need transparency - and if a photo needs transparency, it needs WebP.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Measure before you optimize - the platform may have already solved it
&lt;/h2&gt;

&lt;p&gt;I almost posted "your hero image is 1.2MB" on a supplement store's thread. Then I re-checked with a browser Accept header: Shopify's CDN was auto-converting that PNG to an 84KB WebP for real visitors. The image problem I was about to report didn't exist - the site's actual gaps were metadata and content repetition. Same story on a Ghost blog: the theme was fine (TTFB 50-130ms, proper responsive images); the weight was two membership/payment scripts loading ~900KB on every page view, whether or not the reader would ever subscribe.&lt;/p&gt;

&lt;p&gt;Half the value of measuring is finding problems. The other half is not "fixing" things that were never broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Config leftovers ship to production and nobody notices
&lt;/h2&gt;

&lt;p&gt;My favorite find of the week: a programming blog whose article:publisher meta tag pointed at a beekeeping Facebook page - a copy-paste leftover from another site, shipping in the head of every post. The same blog's og:image was a generic stock photo served from a *.r2.dev URL - Cloudflare's dev-only preview endpoint, rate-limited and not meant for production. Elsewhere: an email popup wired to fire on first scroll, seconds into a first visit; three h1 elements on one page because a hidden brand h1 shipped in the header template.&lt;/p&gt;

&lt;p&gt;None of these show up in Lighthouse. All of them showed up in sixty seconds of reading the raw HTML head. Your head section is small - read it once with your own eyes before launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta-lesson
&lt;/h2&gt;

&lt;p&gt;Speed tools score what they can score. Most of what actually hurt these eight sites - blank share cards, animation-gated heroes, identity metadata borrowed from stock libraries and beehives - scores fine. The audit method that caught everything above is embarrassingly simple: curl the page, read the head, check the wire sizes, load it once cold on a throttled connection, and paste your own URL into a chat app to see the card.&lt;/p&gt;

&lt;p&gt;(All examples are from public feedback threads where owners explicitly asked for critique; numbers are from my measurement logs. I've kept sites anonymous here - the point is the patterns, not the naming.)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>seo</category>
      <category>webperf</category>
    </item>
    <item>
      <title>The most expensive pixel I've ever measured: a 24px arrow that weighed 2.9MB</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Fri, 17 Jul 2026 23:10:53 +0000</pubDate>
      <link>https://dev.to/noguchilin/the-most-expensive-pixel-ive-ever-measured-a-24px-arrow-that-weighed-29mb-3di8</link>
      <guid>https://dev.to/noguchilin/the-most-expensive-pixel-ive-ever-measured-a-24px-arrow-that-weighed-29mb-3di8</guid>
      <description>&lt;p&gt;I run free performance audits on "roast my website" posts. Most findings are boring in a healthy way: oversized JPEGs, render-blocking scripts, a missing cache header. This week I found something I had never seen before, and I want to show you how to check your own site for it, because nothing in your normal workflow will warn you.&lt;/p&gt;

&lt;p&gt;A site I audited transferred 5.2MB across 20 requests on a cold load. More than half of that - 2.9MB on the wire, 8.2MB after decompression - was a single SVG file. It rendered as a navigation arrow. At 24 pixels.&lt;/p&gt;

&lt;p&gt;One arrow. Three megabytes. That's the biggest byte-per-pixel ratio I've ever measured on a production site.&lt;/p&gt;

&lt;h2&gt;
  
  
  How an SVG gets to 8MB
&lt;/h2&gt;

&lt;p&gt;SVG is a text format, so I opened the file expecting to find an embedded raster image (the usual way SVGs get huge - a base64 PNG hiding inside). That's not what this was.&lt;/p&gt;

&lt;p&gt;The file was one single &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; element. Its coordinate list was exported at full machine precision: 15 decimal places per value, including scientific-notation numbers like &lt;code&gt;-3.16856e-06&lt;/code&gt; - for a viewBox that's 37 by 36 units, rendered at 24px.&lt;/p&gt;

&lt;p&gt;Think about what that precision means physically. A 37-unit-wide viewBox drawn at 24px means one unit is smaller than one pixel. Fifteen decimal places of a unit is roughly a millionth of a billionth of a pixel. Every one of those digits is a byte that gets shipped, decompressed, and parsed - and none of them can possibly affect the rendered output, because the output resolution ran out twelve decimal places earlier.&lt;/p&gt;

&lt;p&gt;This is not the site owner's fault, and that's the point of writing this up. Design tools will happily serialize paths at whatever precision their internal math produced. If a curve was generated by a boolean operation or a trace, the coordinates come out looking like floating-point noise - because they are. The exporter doesn't round; the developer pastes the asset in; the icon looks perfect; nobody ever opens a &lt;code&gt;.svg&lt;/code&gt; in a text editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nothing warned them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It looks fine.&lt;/strong&gt; The icon renders crisply. There is no visual symptom at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The site "feels fast" to the owner.&lt;/strong&gt; On the owner's fast connection with a warm cache, the page loaded in about 1.3 seconds. The 3MB tax is only felt by first-time visitors - which for a small business site means exactly the people the site exists to convince.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundlers don't parse SVG geometry.&lt;/strong&gt; Your build tool reports the file size if you look, but nothing flags "this asset is 3000x larger than its information content."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse buries it.&lt;/strong&gt; It shows up inside a generic "reduce payload" line item, indistinguishable from a big hero photo that might be a legitimate choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The two-minute check
&lt;/h2&gt;

&lt;p&gt;Paste this in your browser console on your own site:&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntriesByType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resource&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&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="na"&gt;transferKB&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;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferSize&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;decodedKB&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;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decodedBodySize&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferKB&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferKB&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're scanning the top-10 for two smells:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Any &lt;code&gt;.svg&lt;/code&gt; above ~20KB.&lt;/strong&gt; Icons should be single-digit KB. A 100KB+ SVG is either an embedded raster or precision noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A large decoded-to-transfer ratio on text assets.&lt;/strong&gt; My arrow was 2.9MB transferred but 8.2MB decoded - repetitive digit soup compresses well, which is exactly why it sneaks past "the page doesn't feel that heavy" intuition while still costing real decompression and parse time on a phone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The fix is one command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx svgo arrow.svg &lt;span class="nt"&gt;--precision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-o&lt;/span&gt; arrow.min.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SVGO with 1-2 decimal places of precision is visually lossless for icon-sized art. For this file, the projected result was under 1KB - roughly a 3000x reduction, from more than half the page's weight to a rounding error. If you re-export from the design tool instead, set the export precision to 1 or 2 decimals; the default in several tools is "whatever the float said."&lt;/p&gt;

&lt;p&gt;While you're at it, the same audit pass usually catches the quieter siblings of this bug: a photo shipped at 2048px into a 1300px slot (that site had one - 910KB where ~150KB would do) and a 76KB favicon (also present; ~10x the norm).&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Vector doesn't mean small. SVG size scales with &lt;em&gt;path complexity times export precision&lt;/em&gt;, not with rendered size, and export precision is set by a tool you probably never configured. The cheapest performance win I found all week was, literally, rounding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build a lot of these checks into &lt;a href="https://noguchilin.gumroad.com/l/ywtkug?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=the-most-expensive-pixel-24px-arrow" rel="noopener noreferrer"&gt;SpeedKit&lt;/a&gt;, a Claude Code skill that runs a full speed audit on any site. But honestly: the console snippet above is free and takes two minutes - start there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>svg</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>The biggest text on your homepage might not be a heading. Here's how to check.</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Fri, 17 Jul 2026 23:08:10 +0000</pubDate>
      <link>https://dev.to/noguchilin/the-biggest-text-on-your-homepage-might-not-be-a-heading-heres-how-to-check-319h</link>
      <guid>https://dev.to/noguchilin/the-biggest-text-on-your-homepage-might-not-be-a-heading-heres-how-to-check-319h</guid>
      <description>&lt;p&gt;I free-audit a couple of "roast my website" posts a week. Most of the time the interesting finding is a performance number. This week it was something else: a page where the largest, most important sentence on the entire homepage wasn't marked up as a heading at all.&lt;/p&gt;

&lt;p&gt;Not a missing &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;. Not a skipped heading level. The actual hero line - the one in 48px type that says what the product does - was a plain &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. The only &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; on the page was a small uppercase label floating above it, styled like a tiny category tag.&lt;/p&gt;

&lt;p&gt;To a sighted visitor, the hierarchy reads perfectly: small label, then big headline. To a screen reader or a search crawler, the hierarchy is inverted - the thing that gets first-class semantic weight is the label, and the actual headline is invisible structure, just text in a div.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;I've now seen this exact pattern on three unrelated sites in the same week, so it's worth naming the cause instead of treating it as a one-off typo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design systems separate "what looks big" from "what's semantically first."&lt;/strong&gt; A common layout pattern is: small uppercase eyebrow text above a large display headline. Somewhere in implementation, whichever element the developer reached for first - usually the smaller one, because it's simpler markup - gets the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, and the big display text gets styled as a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; (or a lower heading level) because by the time it's built, "the heading" already exists on the page.&lt;/p&gt;

&lt;p&gt;It's an easy mistake to make precisely because the page &lt;em&gt;looks&lt;/em&gt; right. Nothing renders broken. Lighthouse's automated accessibility checks don't reliably catch it either, because they check for the &lt;em&gt;presence&lt;/em&gt; of a well-formed heading structure, not whether the heading you have is the sentence a human would actually call "the headline."&lt;/p&gt;

&lt;p&gt;I saw the same root cause on a second site this week - a small business site built on a visual website builder. Same shape: a short label had the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, the actual sales headline sat in unmarked text below it. Different stack, same blind spot.&lt;/p&gt;

&lt;p&gt;A third site made an even easier-to-miss version of it: the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; was the site's own brand name in the nav bar (three letters, one of them a logo mark), and the actual hero pitch - the sentence explaining what the product does and why it's different - was marked as an &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; one level down. No visual bug at all; the nav logo has always looked like a nav logo. It's purely a markup choice nobody revisits once the page ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it actually matters
&lt;/h2&gt;

&lt;p&gt;Two concrete costs, not theoretical ones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SEO&lt;/strong&gt;: search engines weight &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; content heavily as a page-topic signal. If your &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; is "Website Monitoring" (three words, generic) and your real value proposition - the sentence that differentiates you from every competitor - lives in an unmarked div, you're handing the crawler your least useful sentence and hiding your best one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt;: a screen reader user navigating by heading (a very common navigation pattern - jumping between &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;/etc.) hears the small label and nothing that tells them what the product actually claims to do. The core pitch of the page is functionally invisible to them.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to check your own site in under a minute
&lt;/h2&gt;

&lt;p&gt;You don't need a crawler for this - a browser tab and view-source is enough:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your homepage, view source (or DevTools -&amp;gt; Elements).&lt;/li&gt;
&lt;li&gt;Find the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag. Read only that text out loud.&lt;/li&gt;
&lt;li&gt;Now look at your hero section with fresh eyes: what's the biggest, most prominent sentence a visitor actually reads first?&lt;/li&gt;
&lt;li&gt;If the answer to 2 and 3 isn't the same text, your heading hierarchy doesn't match your visual hierarchy - and both your SEO and your accessibility are reading the wrong sentence as "the headline."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is almost never a redesign. It's swapping which element carries the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag - a CSS-only change if your class names already control the visual size independent of the semantic tag.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the same kind of check I built into &lt;a href="https://noguchilin.gumroad.com/l/gywwrj?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=the-biggest-text-might-not-be-a-heading" rel="noopener noreferrer"&gt;SEO Audit for Claude Code&lt;/a&gt; - it walks Claude through reading your rendered HTML for exactly this kind of mismatch between what a page shows and what it tells search engines and screen readers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I measured 5 'roast my website' sites in one day. The framework was never the problem.</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Tue, 14 Jul 2026 05:04:24 +0000</pubDate>
      <link>https://dev.to/noguchilin/i-measured-5-roast-my-website-sites-in-one-day-the-framework-was-never-the-problem-14ja</link>
      <guid>https://dev.to/noguchilin/i-measured-5-roast-my-website-sites-in-one-day-the-framework-was-never-the-problem-14ja</guid>
      <description>&lt;p&gt;There's a genre of Reddit post I keep an eye on: "roast my website." Most replies are opinions about colors. I do something different - I open DevTools, measure, and report numbers. Yesterday I did five sites in one day, and the pattern was so consistent it's worth writing down.&lt;/p&gt;

&lt;p&gt;Not one of the five had a framework problem. Every real issue was something the builder had stopped looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Site 1: The SaaS landing page with a 1080px logo in a 28px slot
&lt;/h2&gt;

&lt;p&gt;A polished Next.js landing page, 467KB total - genuinely light. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The nav logo was a 1080x1080 PNG rendered at 28x28. That's ~1,500x more pixels than displayed.&lt;/li&gt;
&lt;li&gt;The favicon was a 48KB SVG - 10% of the entire page weight, in the tab icon.&lt;/li&gt;
&lt;li&gt;The load event fired at 4.4s, almost entirely because of the analytics stack: session recorder, surveys module, exception capture, plus a chat widget iframe still downloading at the 11-second mark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one deserves its own sentence: the site was shipping more tooling to record visitors who don't convert than it would take to fix why they don't convert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Site 2: The cafe page that hides itself for 30 seconds
&lt;/h2&gt;

&lt;p&gt;My favorite of the day. The page finished loading in 2.9 seconds (664KB, network idle). Then a "BREWING THE RUSH... 0% FUELED" preloader sat on top of the finished page for roughly thirty more seconds, crawling to 100% on a timer.&lt;/p&gt;

&lt;p&gt;The loader wasn't loading anything. It was a door held shut with the room ready behind it.&lt;/p&gt;

&lt;p&gt;If you ever add a branded preloader: cap it at 1-2 seconds, and drive it from real load progress. If your page is ready in 3 seconds, the best loading animation is none.&lt;/p&gt;

&lt;h2&gt;
  
  
  Site 3: The joke site that beat everyone
&lt;/h2&gt;

&lt;p&gt;A "roast my SaaS" gag site: 19KB total, 9 requests, loaded in 0.9s, vanilla JS. The only thing I could find to burn was a 360ms TTFB from an un-cached HTML document.&lt;/p&gt;

&lt;p&gt;I mention it because it's the control group. No framework, no build pipeline, no optimization work - and it outperforms almost every funded SaaS landing page I've measured. Weight you never add is the only optimization that's free forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Site 4: The 32MB portfolio
&lt;/h2&gt;

&lt;p&gt;A video production company's portfolio - the site itself is the product demo. Total transfer: 32MB. Two thumbnail JPEGs were 15.9MB &lt;em&gt;each&lt;/em&gt; - 98% of the entire page. They were 2668x3840 exports displayed at roughly 380x680, and one of them wasn't even visible on screen; it downloaded anyway.&lt;/p&gt;

&lt;p&gt;The fix took one sentence: re-export those two files at 800px wide, quality ~80. Page goes from 32MB to under 1MB, visually identical.&lt;/p&gt;

&lt;p&gt;The lesson generalizes: when a page is heavy, it's almost never "everything" - it's two or three files. Sort your network tab by size before you touch anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Site 5: The cinematic site where 4 files missed the pipeline
&lt;/h2&gt;

&lt;p&gt;A beautifully built scroll-story site for a premium food brand: 11.2MB, load event at 15 seconds, TTFB 2.9s. Everyone in the thread was debating the scroll pacing. Nobody had opened the network tab.&lt;/p&gt;

&lt;p&gt;The interesting part: 30 of the site's images were properly compressed WebP. Four photographic backgrounds were PNG - 7.1MB between them. The developer clearly &lt;em&gt;had&lt;/em&gt; an image pipeline; these four files just never went through it.&lt;/p&gt;

&lt;p&gt;That's the most common failure mode I see in real audits, and it's not a knowledge problem. It's an inventory problem. You don't need to learn image optimization - you need to notice which files skipped the process you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Five sites, five different stacks (Next.js x2, vanilla JS, a no-code builder, custom), and every substantive finding was one of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A few oversized files&lt;/strong&gt; - not systemic bloat. Two images were 98% of a 32MB page. Four PNGs were 63% of an 11MB page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-inflicted waiting&lt;/strong&gt; - a fake preloader, an analytics pile, an un-cached HTML document. Time the visitor spends waiting on things that aren't the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assets that skipped an existing pipeline&lt;/strong&gt; - the site already optimizes images; these specific files missed it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this requires expertise to find. It requires opening the network tab, sorting by size, and believing the numbers over your memory of the site. The builders I replied to fixed most of these within a day - one cut their page by 98% overnight.&lt;/p&gt;

&lt;p&gt;Measure before you redesign. It's usually two files.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I do this kind of audit constantly, so I packaged the checklist I use into &lt;a href="https://noguchilin.gumroad.com/l/ywtkug?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=i-measured-5-roast-my-website-sites-in-one-day" rel="noopener noreferrer"&gt;SpeedKit for Claude Code&lt;/a&gt; - it walks Claude through measuring any site the way I did above and outputs the fix list.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>"Passive income with AI" is mostly a lie. Here's the version that actually paid me while I slept.</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Mon, 13 Jul 2026 22:40:16 +0000</pubDate>
      <link>https://dev.to/noguchilin/passive-income-with-ai-is-mostly-a-lie-heres-the-version-that-actually-paid-me-while-i-slept-302a</link>
      <guid>https://dev.to/noguchilin/passive-income-with-ai-is-mostly-a-lie-heres-the-version-that-actually-paid-me-while-i-slept-302a</guid>
      <description>&lt;p&gt;You've seen the videos. "Make $10k/month with AI while you sleep" - and then it's a course, a dropshipping bot, or a content farm that Google will delete from existence next update.&lt;/p&gt;

&lt;p&gt;I want to show you the unsexy version. The one with receipts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This week, money landed in my account while I was literally unconscious.&lt;/strong&gt; Here's the timeline from two nights ago, timestamps from my actual Upwork inbox (I'm in Japan; my clients are in North America - their afternoon is my deep night):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12:38 AM&lt;/strong&gt; - my agent, on its scheduled patrol, finds a job posted 11 minutes earlier. Fewer than 5 proposals. Exactly my niche.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~1:10 AM&lt;/strong&gt; - proposal submitted. The client's screening question is answered, technically and specifically, in the first line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~40 minutes later&lt;/strong&gt; - the client sends an offer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before I wake&lt;/strong&gt; - the agent has already identified their store from public info, mapped their entire product attribute structure, and confirmed the implementation approach. It tells the client: "you'll have the first deliverable within 24h of acceptance."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Morning, coffee in hand&lt;/strong&gt; - I read the shift report, approve the contract with one word, and the escrow is funded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was asleep for every interesting part of that. So: passive income? Let's be precise, because precision is the whole point of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually run
&lt;/h2&gt;

&lt;p&gt;I'm a solo web-performance consultant. Since last week, an AI agent (Claude, wired up with browser access and a written rulebook) runs my operations around the clock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patrols marketplaces every 30-60 minutes.&lt;/strong&gt; Hard filters: job posted within ~30 min, fewer than 5 proposals, payment verified, inside my specialty, and &lt;em&gt;measurable&lt;/em&gt; - if I can't verify the problem with numbers before applying, skip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Investigates before it ever writes a proposal.&lt;/strong&gt; Real example: it identified a client's store from just their company name, before they shared the URL, and mapped 225 product attribute combinations from their public menu markup. The proposal explained &lt;em&gt;how&lt;/em&gt; to solve their exact problem on their exact stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answers clients in minutes at 3 AM.&lt;/strong&gt; Scope questions, measurement results, and - house rule - honest bad news. This week it told a client "your catalog returns 7 products here, not the 8 you expected, and here's exactly why." That message bought more trust than any feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builds, on unpublished drafts, and shows its work.&lt;/strong&gt; Nothing goes live without the client's review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that makes it work: what the AI is banned from doing
&lt;/h2&gt;

&lt;p&gt;This is the boring paragraph every scammy "AI money" video skips, and it's the actual load-bearing wall:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;cannot accept contracts&lt;/strong&gt;. I approve each one, awake, with my own eyes.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;cannot touch money&lt;/strong&gt;. No purchases, no payouts, nothing.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;cannot publish or delete anything irreversible&lt;/strong&gt; - on my accounts or a client's site.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;cannot exaggerate&lt;/strong&gt;. Written rule: only real, verifiable work gets cited. No invented experience, ever. When a subreddit banned one of its too-tidy comments as "AI slop" - fair call - that lesson went into the rulebook instead of into a workaround.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The judgment stays mine. The hours stop being mine. That's the entire trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The receipts, plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;11 proposals sent over ~5 days. 2 contracts won. Both from the "fresh + investigated" pattern. Both landed during my night.&lt;/li&gt;
&lt;li&gt;First job: diagnosed and fixed a WooCommerce checkout, 60% faster, verified with A/B/A tests. Paid, five stars. The review specifically praised that I "actually took the time to investigate" before proposing - the investigation my agent did at 3 PM the client's time, my 4 AM.&lt;/li&gt;
&lt;li&gt;Second job: accepted this morning, escrow funded, first deliverable shipped for review &lt;em&gt;ahead&lt;/em&gt; of the 24h promise.&lt;/li&gt;
&lt;li&gt;Total so far: small. Three figures, not five. I'm building a review base with deliberately small fixed-price jobs, and I'd rather show you real small numbers than fake big ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So is it "passive income"?
&lt;/h2&gt;

&lt;p&gt;Honest answer: &lt;strong&gt;it's not passive - it's leveraged.&lt;/strong&gt; Client work still needs a human who owns the judgment. What changed is brutal and simple: the parts of freelancing that used to punish a solo operator - response speed, timezone coverage, pre-proposal research depth - now run without me. I do minutes of decision-making per day on top of the actual craft.&lt;/p&gt;

&lt;p&gt;And while you read this, it's running. The patrol fires again within the hour. If a client messaged twenty minutes ago, they already have their answer.&lt;/p&gt;

&lt;p&gt;The scam version of this story sells you "money without work."&lt;br&gt;
The real version is better: &lt;strong&gt;work without waste, around the clock, with your name on nothing you wouldn't sign awake.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Want the exact system?&lt;/strong&gt; I packaged the whole night shift - the patrol-loop skill, the screening criteria, the proposal playbook, the failure-modes doc, and the rulebook template that keeps contracts and money human - as the &lt;a href="https://noguchilin.gumroad.com/l/nightshift?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=passive-income-with-ai-is-mostly-a-lie" rel="noopener noreferrer"&gt;Night Shift Kit for Claude Code&lt;/a&gt;. It's built from this real operation, not theory.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I also write up the technical side of these jobs - like &lt;a href="https://dev.to/noguchilin"&gt;cutting a WooCommerce checkout's load time by 60% by removing an optimization plugin&lt;/a&gt;. If your site feels slow: I measure before I touch anything, even before you hire me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>freelancing</category>
      <category>career</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Is your WordPress host the real bottleneck? How to tell in 10 minutes, with numbers</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:43:25 +0000</pubDate>
      <link>https://dev.to/noguchilin/is-your-wordpress-host-the-real-bottleneck-how-to-tell-in-10-minutes-with-numbers-5daj</link>
      <guid>https://dev.to/noguchilin/is-your-wordpress-host-the-real-bottleneck-how-to-tell-in-10-minutes-with-numbers-5daj</guid>
      <description>&lt;p&gt;&lt;em&gt;Everything below is measured on real sites, not quoted from marketing pages. Affiliate disclosure at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Half the "why is my WordPress slow" advice on the internet says &lt;strong&gt;install a cache plugin&lt;/strong&gt;. The other half says &lt;strong&gt;change your host&lt;/strong&gt;. Both are sometimes right - and you can tell which one applies to you in about 10 minutes of measuring. Here is the exact process I use in client audits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Measure TTFB on three kinds of URLs
&lt;/h2&gt;

&lt;p&gt;TTFB (time to first byte) is the part of load time your front-end can never fix. Run each of these 3-5 times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{time_starttransfer}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://yoursite.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A static file&lt;/strong&gt; - &lt;code&gt;/readme.html&lt;/code&gt; exists on almost every WordPress install and bypasses PHP entirely. This is your &lt;em&gt;server's floor&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cacheable page&lt;/strong&gt; - your homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An uncacheable page&lt;/strong&gt; - cart, checkout, or any logged-in page. This is your &lt;em&gt;PHP + database reality&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: Read the pattern
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;readme.html fast (&amp;lt;200ms), homepage fast, checkout 1.5s+?&lt;/strong&gt; Your host serves cached HTML fine, but every dynamic request pays full price: plugin init, database, no object cache. This was exactly the shape of a WooCommerce client I worked with recently - checkout TTFB was 3.3s on shared hosting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything slow, even readme.html?&lt;/strong&gt; The server itself (or its location) is the bottleneck. No plugin will fix this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTFB fine everywhere but the site still feels slow?&lt;/strong&gt; Your problem is front-end (images, JS, fonts) - do not migrate, fix the pages. I wrote up &lt;a href="https://dev.to/noguchilin/i-turned-38-why-is-my-site-slow-audits-into-a-claude-code-skill-2jni"&gt;how I audit that&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Check what your host actually lets you use
&lt;/h2&gt;

&lt;p&gt;Two questions decide whether "optimize in place" is even possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is Redis (or any object cache) available?&lt;/strong&gt; On the client project above, the answer was no - the plan simply did not offer it. Every uncached request rebuilt everything from MySQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is OPcache actually on?&lt;/strong&gt; Verify it - do not assume. (My first hypothesis on that project was "OPcache is off". It was on. Measuring beats guessing in both directions.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answers are "no / can't", you have found a structural ceiling. Cache plugins stacked on top of that ceiling can even make things worse - on that same client, a popular optimization plugin's cache-check overhead was costing 3 seconds per checkout, and &lt;a href="https://dev.to/noguchilin/the-biggest-fix-for-this-slow-woocommerce-checkout-was-removing-an-optimization-plugin-60-472a"&gt;removing it cut load time by 60%&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a healthy baseline looks like (measured)
&lt;/h2&gt;

&lt;p&gt;To have a fair reference point, I spun up a stock WooCommerce store on &lt;a href="https://www.cloudways.com/en/?id=2188197" rel="noopener noreferrer"&gt;Cloudways&lt;/a&gt; (managed WordPress hosting - affiliate link, disclosure below) and measured it from the other side of the Pacific:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL type&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Homepage (cached)&lt;/td&gt;
&lt;td&gt;TTFB &lt;strong&gt;0.6-0.7s&lt;/strong&gt; (including ~180ms of trans-Pacific latency)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart (uncacheable)&lt;/td&gt;
&lt;td&gt;TTFB ~0.9s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse mobile&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;99&lt;/strong&gt; out of the box&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No tuning, no extra plugins - server-side caching, Redis and OPcache just exist by default. That is the point: the shared-host client could not get &lt;em&gt;uncacheable&lt;/em&gt; pages under 2.3s even after real optimization work; a managed baseline starts around 0.9s before any work at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest decision rule
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Front-end problems&lt;/strong&gt; (images/JS/fonts) → fix the pages, don't migrate. Cheaper and faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic pages structurally slow + no object cache available&lt;/strong&gt; → migration buys you more than any plugin can. Budget $14-30/month managed vs $3-8 shared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you do migrate&lt;/strong&gt;: script-check every old URL for 301 integrity, or your rankings pay for the speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclosure: the Cloudways link above is an affiliate link. I only recommend it because I measured it myself - the numbers in the table are mine, not theirs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webperf</category>
      <category>hosting</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The biggest fix for this slow WooCommerce checkout was removing an optimization plugin (-60%, measured)</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Mon, 13 Jul 2026 04:13:28 +0000</pubDate>
      <link>https://dev.to/noguchilin/the-biggest-fix-for-this-slow-woocommerce-checkout-was-removing-an-optimization-plugin-60-472a</link>
      <guid>https://dev.to/noguchilin/the-biggest-fix-for-this-slow-woocommerce-checkout-was-removing-an-optimization-plugin-60-472a</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a real diagnosis for a real client. The site, niche, and anything identifying are deliberately left out. Every number is measured, not estimated.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A client's WooCommerce checkout took &lt;strong&gt;6-8 seconds&lt;/strong&gt;. A default theme dropped it to 1-2s, so "it's the theme" seemed obvious. It wasn't that simple.&lt;/li&gt;
&lt;li&gt;My first hypothesis (PHP OPcache disabled) was &lt;strong&gt;wrong&lt;/strong&gt;, and I told the client so instead of quietly finding a different smoking gun.&lt;/li&gt;
&lt;li&gt;Profiling split the cost three ways: &lt;strong&gt;~58% plugin/WooCommerce init, ~14% a synchronous API call, ~22% page-builder rendering&lt;/strong&gt;. I told the client further plugin cleanup would be worth "a few hundred milliseconds."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;That expectation was wrong too - in the good direction.&lt;/strong&gt; Controlled A/B/A tests found one plugin whose deactivation took checkout from &lt;strong&gt;4.6-4.7s to 1.68-1.73s (-60%)&lt;/strong&gt;. It was a &lt;em&gt;performance optimization&lt;/em&gt; plugin.&lt;/li&gt;
&lt;li&gt;Total silver bullets sold: zero. Total plugins removed: one - an optimizer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Client's report: checkout takes 6-8 seconds on their WooCommerce store (heavyweight theme + WPBakery page builder). They'd already tried the obvious things - upgraded hosting, removed some plugins, simplified the checkout page. Still slow. Their own test: switch to a default WordPress theme, checkout drops to 1-2 seconds. Switch back, 6-8s again.&lt;/p&gt;

&lt;p&gt;That test is genuinely useful data. It rules out "it's the network" and points hard at &lt;strong&gt;something in the theme/plugin stack&lt;/strong&gt;. But "the theme is slow" isn't a diagnosis - it's a direction to start digging.&lt;/p&gt;

&lt;h2&gt;
  
  
  First hypothesis, and why I dropped it
&lt;/h2&gt;

&lt;p&gt;My first read: PHP's opcode cache (OPcache) disabled, forcing the server to recompile a large theme + page-builder codebase on every request. It fit the symptoms, and it's a real, common cause on cheap hosting.&lt;/p&gt;

&lt;p&gt;I said as much to the client, then went to actually check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It wasn't that.&lt;/strong&gt; OPcache was on and working. I could have quietly moved on and let the client believe "OPcache was the fix" once I found something else. I didn't, because the whole point of a paid diagnosis is that it's &lt;em&gt;actually&lt;/em&gt; the reason, not a plausible-sounding one. Saying "my first theory was wrong, here's what's actually happening" costs a little credibility in the moment and buys a lot of it over the relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually did
&lt;/h2&gt;

&lt;p&gt;Cloned the live site to a private staging copy (nothing touches production during diagnosis), then used &lt;strong&gt;Query Monitor&lt;/strong&gt; to break down where page-generation time was going, plus temporary custom timers around suspect hooks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Clone to staging, never touch production during diagnosis
[ ] Profile with Query Monitor: hook timings, query count, memory
[ ] Add temporary timers around suspect init/render stages
[ ] Rule things out explicitly (OPcache, Redis) - measure, don't assume
[ ] Test plugin changes as A/B/A with repeats, not A/B once
[ ] Only ship a fix once it's measured
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ruling things out matters as much as finding the cause. Redis? Not available on that hosting plan at all (just an unused client library). Another plausible-sounding fix, closed off with one command instead of a paragraph of speculation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the time actually was
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;th&gt;What's really happening&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugin / WooCommerce init&lt;/td&gt;
&lt;td&gt;~58% (~3s)&lt;/td&gt;
&lt;td&gt;Every active plugin runs its setup code on every request - combined weight, not one villain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synchronous API call&lt;/td&gt;
&lt;td&gt;~14% (~0.7s)&lt;/td&gt;
&lt;td&gt;A payment-gateway status check firing on every checkout load; it only needs to run occasionally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page-builder rendering&lt;/td&gt;
&lt;td&gt;~22% (~1.1s)&lt;/td&gt;
&lt;td&gt;Theme + page builder assembling the checkout form&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The API call was the first clean win: a read-only status check, safe to cache briefly. Shipped immediately for ~0.2s. Small, free, zero-risk.&lt;/p&gt;

&lt;p&gt;Based on the profile, I told the client the remaining plugin audit would be worth &lt;strong&gt;"a few hundred milliseconds, not a miracle."&lt;/strong&gt; I put that expectation in writing. Keep that sentence in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist: the optimizer was the bottleneck
&lt;/h2&gt;

&lt;p&gt;The client greenlit a controlled plugin test on staging. Method: deactivate one suspect, measure checkout 3x, reactivate, measure again (A/B/A), so a hosting hiccup can't masquerade as a result.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Suspect #1 (a checkout-flow plugin): &lt;strong&gt;no measurable difference.&lt;/strong&gt; Deactivating it would have been a guess dressed as a fix.&lt;/li&gt;
&lt;li&gt;Suspect #2: &lt;strong&gt;WP-Optimize - checkout went from 4.6-4.7s to 1.68-1.73s. Minus 60%, reproducible across all three A/B/A rounds&lt;/strong&gt;, with the payment form still working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why would a &lt;em&gt;performance&lt;/em&gt; plugin cost 3 seconds? Its page-cache layer still runs its checking/processing logic on pages that &lt;strong&gt;can never be cached&lt;/strong&gt; - cart and checkout are dynamic by definition. So on exactly the pages that were hurting, it contributed pure overhead. The site also had a second caching plugin for the same job, so the two overlapped.&lt;/p&gt;

&lt;p&gt;I had to go back to the client and say my written estimate of "a few hundred milliseconds" was wrong by an order of magnitude. Being wrong in that direction is pleasant, but the lesson is the same as with OPcache: &lt;strong&gt;the profile tells you where to dig, only the controlled test tells you what's true.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Production results after shipping both fixes: server response time went from &lt;strong&gt;3.3s to 2.35s&lt;/strong&gt; on the live checkout (the remaining gap vs staging is the structural cost below). The client tested it live and released the milestone the same morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still slow (the honest part)
&lt;/h2&gt;

&lt;p&gt;The remaining ~2s is not a bug. It's WooCommerce + a page-builder-heavy theme + a normal-sized plugin stack, all initializing on every request, on shared hosting where Redis isn't available and PHP workers are limited. That gets better with structural choices - a leaner theme, fewer always-on plugins, or hosting where object caching actually exists - not with another patch. If your host doesn't offer Redis at all (as here), that's not something any plugin can fix. A managed WordPress host like &lt;a href="https://www.cloudways.com/en/?id=2188197" rel="noopener noreferrer"&gt;Cloudways&lt;/a&gt; gives you Redis + OPcache out of the box - I spun up a WooCommerce store on it and measured 0.6-0.7s TTFB on cached pages, ~0.9s on the uncacheable cart page, and Lighthouse mobile 99, from the other side of the Pacific (affiliate link, disclosure below).&lt;/p&gt;

&lt;p&gt;There's no "install this plugin and get 2 seconds." If someone tells you there is, ask for the before/after numbers, not the promise. Sometimes the numbers will surprise you - this time the 2 seconds came from &lt;em&gt;uninstalling&lt;/em&gt; one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A theme-swap test is good evidence, not a diagnosis. It tells you &lt;em&gt;where&lt;/em&gt; to look, not &lt;em&gt;what's&lt;/em&gt; there.&lt;/li&gt;
&lt;li&gt;Say the wrong hypothesis out loud, then check it anyway. Twice, in this case - OPcache (wrong villain) and "a few hundred ms" (wrong ceiling).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance plugins are plugins.&lt;/strong&gt; They have per-request overhead like everything else, and on uncacheable pages a page cache is all cost, no benefit. Two caching plugins doing one job is a smell.&lt;/li&gt;
&lt;li&gt;A/B once is an anecdote; A/B/A three times with a working payment form is a result you can ship.&lt;/li&gt;
&lt;li&gt;Ship the small safe win immediately. Scope the structural conversation separately - and honestly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclosure: the Cloudways link above is an affiliate link. I only link to it because it solves the exact hosting limitation described in this post, and I tested it myself before recommending it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>woocommerce</category>
      <category>webperf</category>
      <category>php</category>
    </item>
    <item>
      <title>The on-page SEO problems you can't see are in the HTML — so I made Claude Code read it</title>
      <dc:creator>NOGUCHILin</dc:creator>
      <pubDate>Sat, 11 Jul 2026 02:12:40 +0000</pubDate>
      <link>https://dev.to/noguchilin/the-on-page-seo-problems-you-cant-see-are-in-the-html-so-i-made-claude-code-read-it-k0e</link>
      <guid>https://dev.to/noguchilin/the-on-page-seo-problems-you-cant-see-are-in-the-html-so-i-made-claude-code-read-it-k0e</guid>
      <description>&lt;p&gt;A couple of weeks ago I built a Claude Code skill that measures &lt;em&gt;why a page is slow&lt;/em&gt; (SpeedKit). While I was auditing all those "roast my site" pages, I kept noticing something: the SEO problems were as invisible as the speed ones — and just as mechanical to check.&lt;/p&gt;

&lt;p&gt;Not keyword strategy. Not backlinks. The boring, technical stuff that quietly keeps a page out of search or makes it look broken in the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an accidental &lt;code&gt;noindex&lt;/code&gt; in the robots meta — the page is literally telling Google to drop it, and nobody notices&lt;/li&gt;
&lt;li&gt;a canonical pointing at a &lt;em&gt;different&lt;/em&gt; URL than the one you want ranked&lt;/li&gt;
&lt;li&gt;a 195-character meta description that gets truncated with an ellipsis in the SERP&lt;/li&gt;
&lt;li&gt;a title cut off at 60 chars, or zero/multiple H1s&lt;/li&gt;
&lt;li&gt;missing Open Graph tags, so the link renders bare and unclickable-looking when shared on Slack/X/LinkedIn&lt;/li&gt;
&lt;li&gt;JSON-LD structured data that's &lt;em&gt;present but malformed&lt;/em&gt;, so it does nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is readable straight from the page's HTML. You don't need a crawler subscription or an API key — you need to actually look. Which is exactly the kind of mechanical, every-time checklist Claude Code is good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I made it read the page
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;onpage-seo-audit&lt;/code&gt; is a SKILL.md plus a &lt;code&gt;seo-measure.js&lt;/code&gt; that runs in the page context and reads: the title and its length, the meta description length, H1 count and heading order, the canonical (and whether it points at the current URL), the robots meta, Open Graph + Twitter cards, JSON-LD blocks and their &lt;code&gt;@type&lt;/code&gt;, image alt coverage, &lt;code&gt;lang&lt;/code&gt;/viewport/charset, internal-vs-external links, body word count, and it fetches &lt;code&gt;/robots.txt&lt;/code&gt; and &lt;code&gt;/sitemap.xml&lt;/code&gt; to check crawlability.&lt;/p&gt;

&lt;p&gt;It returns a ranked report — indexability blockers first — where every finding is a &lt;strong&gt;real value read from the page&lt;/strong&gt;, not a guess.&lt;/p&gt;

&lt;p&gt;Here's the shape of the canonical check, which is my favorite because it catches a genuinely sneaky bug:&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;canonicalHref&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link[rel="canonical"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canonicalHref&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;canonicalHref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;$/&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;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&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;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;$/&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;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// self === false → the page says the "real" version is a *different* URL.&lt;/span&gt;
&lt;span class="c1"&gt;// Sometimes that's correct (www → non-www consolidation). Sometimes you've&lt;/span&gt;
&lt;span class="c1"&gt;// just told Google to de-index the page you're looking at. Worth a human look.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part that mattered most: keeping it honest
&lt;/h2&gt;

&lt;p&gt;The whole thing only works if the model reports what it measured and nothing else. The SKILL.md is explicit about it: report the real character count, the real flag, the real ratio — never invent a metric you didn't read. And when a "problem" is plausibly intentional (a canonical consolidating www → non-www, a deliberately short landing page), say &lt;em&gt;"verify this"&lt;/em&gt; rather than &lt;em&gt;"this is broken."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It also refuses to promise rankings. On-page SEO removes the technical reasons a page &lt;em&gt;can't&lt;/em&gt; be indexed or &lt;em&gt;looks wrong&lt;/em&gt; in search. That's the honest scope — no keyword-density myths, no "submit to 500 directories."&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the pattern on your own site
&lt;/h2&gt;

&lt;p&gt;Open DevTools → Console on any page and check the list at the top by hand — you'll probably find at least one. That's most of the value right there.&lt;/p&gt;

&lt;p&gt;If you'd rather not remember to, I packaged it (audit + a fix skill that applies the corrections and re-reads the page) as &lt;strong&gt;SEO Audit for Claude Code&lt;/strong&gt; ($19): &lt;a href="https://noguchilin.gumroad.com/l/gywwrj?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=the-on-page-seo-problems-you-cant-see" rel="noopener noreferrer"&gt;https://noguchilin.gumroad.com/l/gywwrj?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=the-on-page-seo-problems-you-cant-see&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's the sibling of the speed one — &lt;a href="https://noguchilin.gumroad.com/l/ywtkug?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=the-on-page-seo-problems-you-cant-see" rel="noopener noreferrer"&gt;SpeedKit&lt;/a&gt; reads &lt;em&gt;why a page is slow&lt;/em&gt;, this one reads &lt;em&gt;why it can't be found.&lt;/em&gt; Two halves of the same "is this site technically healthy" question.&lt;/p&gt;

&lt;p&gt;Happy to run the audit on your page if you drop a URL in the comments.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>claude</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
