<?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: Artur Smirnov</title>
    <description>The latest articles on DEV Community by Artur Smirnov (@smirnovartur).</description>
    <link>https://dev.to/smirnovartur</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%2F4068031%2F9065477c-579d-45f4-9d86-d75f58a50e8f.png</url>
      <title>DEV Community: Artur Smirnov</title>
      <link>https://dev.to/smirnovartur</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smirnovartur"/>
    <language>en</language>
    <item>
      <title>A lattice with no mesh: sphere-tracing a signed distance field in raw WebGL2</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Tue, 11 Aug 2026 14:49:56 +0000</pubDate>
      <link>https://dev.to/smirnovartur/a-lattice-with-no-mesh-sphere-tracing-a-signed-distance-field-in-raw-webgl2-2g9h</link>
      <guid>https://dev.to/smirnovartur/a-lattice-with-no-mesh-sphere-tracing-a-signed-distance-field-in-raw-webgl2-2g9h</guid>
      <description>&lt;p&gt;Live page: &lt;a href="https://smirnov-artur.github.io/webgl/lattice/" rel="noopener noreferrer"&gt;https://smirnov-artur.github.io/webgl/lattice/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's no 3D model behind this page — no mesh, no textures, no &lt;code&gt;.glb&lt;/code&gt; file. The lattice is a &lt;strong&gt;graded gyroid&lt;/strong&gt;, a triply-periodic minimal surface, and it exists purely as a signed distance function (SDF): a formula that, given any point in space, returns how far that point is from the surface. The whole scene is one WebGL2 fragment shader that "sphere-traces" that function — walks a ray forward by the returned distance, over and over, until it's close enough to the surface to count as a hit. Raw WebGL2, no three.js, no library at all: one inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, 31.8 KB of source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The geometry is a lie, in the best way
&lt;/h2&gt;

&lt;p&gt;The only thing actually submitted to the GPU is a single full-screen triangle, generated in the vertex shader straight from &lt;code&gt;gl_VertexID&lt;/code&gt; — no vertex buffer, no attributes, nothing to upload. The page's own on-screen HUD confirms it: &lt;code&gt;geometry: 0 B&lt;/code&gt;. Every bit of surface you see on the lattice sphere is computed per-pixel in the fragment shader, not stored anywhere.&lt;/p&gt;

&lt;p&gt;I wrapped &lt;code&gt;gl.drawArrays&lt;/code&gt; to count how many times it's actually called per frame: &lt;strong&gt;7&lt;/strong&gt; — one for the SDF scene pass, and six for the post-processing chain (a bright-pass, a two-level separable Gaussian blur, then a composite that does ACES tonemapping, per-channel chromatic aberration, vignette, and film grain). The HUD's own &lt;code&gt;draw&lt;/code&gt; / &lt;code&gt;passes&lt;/code&gt; readouts agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it look like metal, not plastic
&lt;/h2&gt;

&lt;p&gt;The raymarch itself wasn't the hard part — the hard part was getting the surface to read as tempered steel instead of gray plastic. Two tricks did most of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dispersion per color channel.&lt;/strong&gt; Instead of one refraction path, the shader traces three slightly offset paths through the same field, one per channel — that's what gives the edges the faint color fringe you'd expect from real glass or stressed metal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radial grading, one &lt;code&gt;smoothstep&lt;/code&gt;.&lt;/strong&gt; The lattice cell size shrinks toward the core and opens up at the skin — dense in the middle, sparse at the edge — driven by a single &lt;code&gt;smoothstep&lt;/code&gt; on the cell frequency term. No second field, no extra noise layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Numbers, measured on this page, not claimed
&lt;/h2&gt;

&lt;p&gt;I don't trust "buttery smooth" as a spec, so here's what I actually measured on this machine (RTX 3050, 1250×1276 canvas, DPR 1) — the page's own live HUD agrees with all of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;60 fps steady, vsync-capped&lt;/strong&gt; — confirmed two ways: the page's own EMA-smoothed frame-time readout, and an independent &lt;code&gt;requestAnimationFrame&lt;/code&gt; counter over a 6-second steady-state window&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~65 KB total transferred&lt;/strong&gt; for the entire page — 15 KB gzip for HTML+CSS+JS+shaders combined, ~50 KB for two subsetted variable fonts, zero images, zero model files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 quality tiers&lt;/strong&gt; (56 / 80 / 88 raymarch steps for low/mid/high), chosen automatically: first a probe for a software-rasterizer fallback (drops straight to low), then touch input + screen size (drops to mid)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;Antialiasing is off on purpose — the shader leans on device-pixel-ratio and the post pass instead of MSAA, so if you force DPR down, the lattice edges alias visibly. If WebGL2 isn't available at all, there's a static fallback panel instead of the canvas; I've only verified that path on evergreen browsers, not on anything old enough to actually need it. And running a raymarcher flat-out at 60fps draws real, continuous GPU power — I haven't added a frame cap or measured battery impact on a laptop.&lt;/p&gt;

&lt;p&gt;Happy to go deeper into the distance field, the tiering logic, or the dispersion shader if anyone's curious.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webgl</category>
      <category>showdev</category>
    </item>
    <item>
      <title>'Thanks for contacting us' is not proof: four ways a website message disappears</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Sat, 08 Aug 2026 07:02:21 +0000</pubDate>
      <link>https://dev.to/smirnovartur/thanks-for-contacting-us-is-not-proof-four-ways-a-website-message-disappears-90j</link>
      <guid>https://dev.to/smirnovartur/thanks-for-contacting-us-is-not-proof-four-ways-a-website-message-disappears-90j</guid>
      <description>&lt;p&gt;I audit websites from the outside and then write to whoever owns them, which means I spend a lot of&lt;br&gt;
time in other people's contact channels — forms, chat widgets, published addresses. Over one week&lt;br&gt;
that came to seventy-odd contact pages and around a hundred messages.&lt;/p&gt;

&lt;p&gt;The thing I did not expect to learn: &lt;strong&gt;whether a message arrives is a completely separate question&lt;br&gt;
from whether the form works.&lt;/strong&gt; Four failure modes, all measured, all of them holes that a site owner&lt;br&gt;
cannot see from the inside, because from the inside everything looks fine.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. The careers mailbox turns customers into applicants
&lt;/h2&gt;

&lt;p&gt;Four of my letters went to addresses of the form &lt;code&gt;jobs@&lt;/code&gt;, &lt;code&gt;careers@&lt;/code&gt;, &lt;code&gt;bewerbung@&lt;/code&gt;, &lt;code&gt;karriere@&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All four came back as job applications. Not one exception.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Thanks so much for your interest in &lt;strong&gt;joining our team&lt;/strong&gt;… unable to reply"&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Empfangsbestätigung Ihrer Bewerbung&lt;/code&gt; — confirmation of receipt of your application&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second one is the one to sit with. That message was about the company's &lt;strong&gt;broken product&lt;br&gt;
configurator&lt;/strong&gt; on their own website. The reply was a receipt confirming my application for a job.&lt;/p&gt;

&lt;p&gt;The mechanism is an applicant tracking system. &lt;code&gt;careers@&lt;/code&gt; is not a mailbox with a person behind it,&lt;br&gt;
it is an intake pipe: everything that lands there becomes a candidate record with a status field, and&lt;br&gt;
nobody in sales or engineering will ever open it. Two of the four were the same well-known hiring&lt;br&gt;
platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this hits site owners specifically:&lt;/strong&gt; the careers address is the easiest address on the whole&lt;br&gt;
site to find. It is on the jobs page, in the footer, in the structured data, and it is indexed. It is&lt;br&gt;
frequently the &lt;em&gt;only&lt;/em&gt; address a company publishes as plain text, because the sales route is a form.&lt;br&gt;
So an outsider with something useful to tell you — a customer, a supplier, a stranger who found a bug&lt;br&gt;
in your checkout — reaches for the address that is visible, and it is the one address in the company&lt;br&gt;
where their message provably will not be read.&lt;/p&gt;

&lt;p&gt;Worth ten minutes of your time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is there a non-careers address published in plain text anywhere on your site? If the only reachable
string is &lt;code&gt;careers@&lt;/code&gt;, that is your business channel now, whether you meant it or not.&lt;/li&gt;
&lt;li&gt;Does your ATS auto-reply to everything, including messages that are obviously not applications?&lt;/li&gt;
&lt;li&gt;Does anyone read the "unqualified" bucket in the ATS? That is where your bug report lives.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  2. Captcha markup is not captcha
&lt;/h2&gt;

&lt;p&gt;I nearly discarded most of a channel over this one.&lt;/p&gt;

&lt;p&gt;Census of seventy contact pages: markers for &lt;code&gt;hcaptcha&lt;/code&gt; or &lt;code&gt;recaptcha&lt;/code&gt; present in the HTML of&lt;br&gt;
&lt;strong&gt;sixty-five&lt;/strong&gt;. Visibly rendered captcha widgets: &lt;strong&gt;zero&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On some hosted platforms an invisible captcha is mixed into every form by default and switched on&lt;br&gt;
selectively — the markup is a platform default, not a decision anyone made about that shop. Filtering&lt;br&gt;
by "the page contains the string &lt;code&gt;hcaptcha&lt;/code&gt;" would have thrown away ninety-three per cent of the&lt;br&gt;
channel, including every shop whose form happily accepted my message and replied with a confirmation.&lt;/p&gt;

&lt;p&gt;What actually blocks a form is a &lt;strong&gt;rendered&lt;/strong&gt; widget: a visible &lt;code&gt;h-captcha&lt;/code&gt; / &lt;code&gt;g-recaptcha&lt;/code&gt; container&lt;br&gt;
or an actual hCaptcha iframe on the page. That costs one request to check, against a string match&lt;br&gt;
that is worse than useless because it is confidently wrong.&lt;/p&gt;

&lt;p&gt;Two related things, both measured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Turnstile in managed mode leaves &lt;code&gt;cf-turnstile-response&lt;/code&gt; empty until submit.&lt;/strong&gt; I
watched a token field stay blank for sixty seconds and marked the form unreachable. It was not: the
token is fetched at submission time. On another site I had written off as "Turnstile, no way
through", the challenge solved itself in a normal browser tab — an 837-character token appeared a
few seconds after load, the form went, and the page said "Thank you — Your form has been submitted
successfully". Waiting for the token before clicking is pointless. Check the result, not the field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invisible reCAPTCHA (&lt;code&gt;data-size="invisible"&lt;/code&gt;) and the Enterprise variant are not barriers by
design&lt;/strong&gt; — no human step exists in the mechanism. A visible v2 checkbox is a different matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For an owner this cuts both ways.&lt;/strong&gt; If you added a captcha because of spam, verify it renders at&lt;br&gt;
all — you may be protected by a string in your HTML and nothing else. And if it does render, know&lt;br&gt;
what it costs: a visible challenge on a B2B contact form is a toll charged to every genuine inquiry,&lt;br&gt;
in exchange for stopping bots that mostly are not stopped by it.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Your success indicator is probably lying
&lt;/h2&gt;

&lt;p&gt;I need to know whether a message went, so I have become uncomfortably well informed about how forms&lt;br&gt;
signal success. Three real cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fields cleared, URL changed, no message sent that I could prove.&lt;/strong&gt; The submit went through, the
connection dropped on the same action, everything looked like a successful navigation. I logged it
as unconfirmed and did not resend, because a duplicate is worse than a miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fields stayed filled, no error, no success.&lt;/strong&gt; Two attempts, &lt;code&gt;checkValidity()&lt;/code&gt; true, form intact.
The click had been dispatched to a background renderer — the button was "pressed" in a tab that was
not in front. No error anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four forms redirected to &lt;code&gt;?contact_posted=true&lt;/code&gt;&lt;/strong&gt; and showed "Your e-mail has been sent." I
checked, and without that URL parameter the confirmation string does not exist on the page at all.
The confirmation is a &lt;em&gt;consequence&lt;/em&gt; of the parameter, not evidence of delivery on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only thing I now accept as proof is &lt;strong&gt;confirmation text rendered on the page after submission.&lt;/strong&gt;&lt;br&gt;
Cleared fields lie. A URL change lies. A missing error message proves nothing whatsoever: the absence&lt;br&gt;
of a failure signal is not the presence of a success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For owners:&lt;/strong&gt; go and submit your own contact form and watch what the page tells you. If the answer&lt;br&gt;
is "it scrolls to the top and the fields are empty", a meaningful share of your visitors will&lt;br&gt;
conclude it failed. They will submit again, or they will leave and not come back. Say the word&lt;br&gt;
"sent", in a sentence, where the fields used to be.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Delivered is not delivered
&lt;/h2&gt;

&lt;p&gt;Bounces come in two shapes and one of them is easy to miss entirely.&lt;/p&gt;

&lt;p&gt;The ordinary one is &lt;code&gt;mailer-daemon&lt;/code&gt;. The other arrives from &lt;strong&gt;&lt;code&gt;postmaster@&lt;/code&gt; on the recipient's own&lt;br&gt;
domain&lt;/strong&gt;, and it means the published address forwards to somebody's personal mailbox and the&lt;br&gt;
forwarding broke. A search for &lt;code&gt;mailer-daemon&lt;/code&gt; alone does not see it. This one does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;newer_than:7d (from:mailer-daemon OR from:postmaster OR subject:undeliverable
  OR subject:undelivered OR "delivery has failed")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three more findings from the same pile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;554 5.7.1 Relay access denied&lt;/code&gt;&lt;/strong&gt; from an address published on the company's own contact page.
That mailbox does not accept external mail at all. Nobody there knows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business auto-replies land in Promotions.&lt;/strong&gt; A manufacturer's "Your request to us" receipt, with a
printout of the fields I had filled in, was sitting in the Promotions tab and never in the inbox. I
had recorded that delivery as unproven for a day. If your own order confirmations or inquiry
receipts go out with marketing headers, this is happening to your customers too — and they will not
go looking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some hosted form products reject free mail domains.&lt;/strong&gt; &lt;code&gt;@gmail.com&lt;/code&gt; was rejected outright by two
different vendors' contact forms. As an anti-spam measure it is understandable. As a filter on your
inbound it is brutal: a large share of small-business owners, sole traders and individual customers
have no other address.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The ten-minute check on your own site
&lt;/h2&gt;

&lt;p&gt;Not from your office network, and not from the account that owns the site:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your contact page &lt;strong&gt;on a phone, on mobile data, in a private window.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Submit a real message using a &lt;strong&gt;Gmail address&lt;/strong&gt;. Watch what the page says afterwards, word for
word.&lt;/li&gt;
&lt;li&gt;See where it lands — inbox, Promotions, spam — and how long it takes.&lt;/li&gt;
&lt;li&gt;Follow every publicly visible address on your site and check what is on the other end: a mailbox
someone opens, or an intake system.&lt;/li&gt;
&lt;li&gt;Send one message to each of those addresses &lt;strong&gt;from outside your own domain&lt;/strong&gt; and see which ones
come back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every one of the four failures above is invisible from inside the company and every one of them&lt;br&gt;
costs inbound. The first three cost you the message. The fourth costs you the message and tells the&lt;br&gt;
sender it went.&lt;/p&gt;




&lt;p&gt;If you run the ten-minute check and something in it surprises you, I would be glad to hear which&lt;br&gt;
one — I am collecting failure modes and I doubt these four are the last of them.&lt;/p&gt;

&lt;p&gt;I do outside audits of sites and storefronts, and the repairs that follow.&lt;br&gt;
Portfolio: &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt; ·&lt;br&gt;
Telegram &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; · &lt;code&gt;paladei702@gmail.com&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ux</category>
      <category>debugging</category>
      <category>marketing</category>
    </item>
    <item>
      <title>The image loads and stays invisible: the opacity: 0 is on the wrapper, not the image</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Sat, 08 Aug 2026 06:57:03 +0000</pubDate>
      <link>https://dev.to/smirnovartur/the-image-loads-and-stays-invisible-the-opacity-0-is-on-the-wrapper-not-the-image-1ol8</link>
      <guid>https://dev.to/smirnovartur/the-image-loads-and-stays-invisible-the-opacity-0-is-on-the-wrapper-not-the-image-1ol8</guid>
      <description>&lt;p&gt;Someone posted this on a support forum and got no answers for a week:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The featured image is displayed correctly on desktop browsers and iPad (Chrome and Safari), but&lt;br&gt;
the featured image is missing on iPhone. […] HTML output contains the &lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;&lt;br&gt;
element […] No JavaScript errors in the browser console. CSS shows normal values (display: block,&lt;br&gt;
correct height, opacity 1).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every sentence of that is true, carefully checked, and points away from the cause.&lt;/p&gt;

&lt;p&gt;The image is in the markup. The image is downloaded. &lt;code&gt;getComputedStyle&lt;/code&gt; on the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; returns&lt;br&gt;
&lt;code&gt;display: block&lt;/code&gt;, a correct height, &lt;code&gt;opacity: 1&lt;/code&gt;. Nothing is wrong with the image, and he had spent&lt;br&gt;
a week establishing that with increasing precision.&lt;/p&gt;

&lt;p&gt;It took me one look at the page source to find it, and only because I was looking one level up.&lt;/p&gt;
&lt;h2&gt;
  
  
  What was actually there
&lt;/h2&gt;

&lt;p&gt;The theme wraps the featured image in a &lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt; and puts an animation class on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;figure&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ext-animate--on wp-block-post-featured-image"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"…"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the stylesheet, exactly one rule:&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;.ext-animate--on&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.ext-animate--off&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The class is removed by the theme's own JavaScript once the element scrolls into view. That is the&lt;br&gt;
entire fade-in mechanism: start at zero, let a script take the class off, the CSS transition does the&lt;br&gt;
rest.&lt;/p&gt;

&lt;p&gt;If that script does not run, or does not reach this element, the wrapper stays at &lt;code&gt;opacity: 0&lt;/code&gt;&lt;br&gt;
forever. The image inside it is loaded, laid out, correct in every respect, and invisible — because&lt;br&gt;
you cannot see through a transparent parent no matter how opaque the child is.&lt;/p&gt;

&lt;p&gt;His own investigation could not find this, and it is worth being precise about why: &lt;strong&gt;he inspected&lt;br&gt;
the image, and the zero was on the figure.&lt;/strong&gt; Every value he read was genuinely normal. The tooling&lt;br&gt;
told him the truth about the wrong element.&lt;/p&gt;

&lt;p&gt;(The device-specific part has a likely explanation in the same file: the theme respects&lt;br&gt;
&lt;code&gt;prefers-reduced-motion&lt;/code&gt;. If "Reduce Motion" is on in iOS accessibility settings, the reveal can be&lt;br&gt;
suppressed while the starting state stays put — visible everywhere, invisible on exactly one phone.&lt;br&gt;
That one I flagged as a suspicion rather than a measurement, because I could not reproduce it on his&lt;br&gt;
device.)&lt;/p&gt;
&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;Anything that hides an element hides its subtree, and none of it shows up on the child:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;on an ancestor&lt;/th&gt;
&lt;th&gt;the child's own computed style&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opacity: 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;opacity: 1&lt;/code&gt; — untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;visibility: hidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inherited, so this one &lt;em&gt;does&lt;/em&gt; show&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;height: 0&lt;/code&gt; + &lt;code&gt;overflow: hidden&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;correct height, correct everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transform: scale(0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clip-path: inset(100%)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content-visibility: hidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a &lt;code&gt;::before&lt;/code&gt; overlay painted on top&lt;/td&gt;
&lt;td&gt;nothing at all, in any property&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So when an element is loaded, laid out, and not on screen, stop reading its style and walk up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;whyInvisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;n&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&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;bad&lt;/span&gt; &lt;span class="o"&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="o"&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;opacity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opacity:0&lt;/span&gt;&lt;span class="dl"&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibility&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibility:hidden&lt;/span&gt;&lt;span class="dl"&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;display:none&lt;/span&gt;&lt;span class="dl"&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;matrix(0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transform scaled to 0&lt;/span&gt;&lt;span class="dl"&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipPath&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clip-path:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&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;clipPath&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&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;height&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`box &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;width&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="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;whyInvisible&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;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;img.your-image&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;Run that, and the answer is a row in a table instead of a week of forum silence.&lt;/p&gt;

&lt;p&gt;One more habit worth building, because it costs nothing: when a fade-in is involved, check the&lt;br&gt;
element with JavaScript disabled. Reveal-on-scroll animations written as "start hidden, let a script&lt;br&gt;
un-hide" have a failure mode with no error message — the script that was supposed to fix it simply&lt;br&gt;
never ran, and the page looks like the CSS is broken. If your site does this, the starting state&lt;br&gt;
should be visible and the animation should be an enhancement, not the other way round.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half: when the image is fine and your &lt;em&gt;measurement&lt;/em&gt; is broken
&lt;/h2&gt;

&lt;p&gt;I audit sites from the outside and write to the owners, which means my detector has to be right&lt;br&gt;
before I put a claim in an email. Three of the ways it lied to me, all on live sites, all inside one&lt;br&gt;
week:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Eleven broken images."&lt;/strong&gt; The detector flagged every image where &lt;code&gt;naturalWidth === 0&lt;/code&gt; after&lt;br&gt;
scrolling to the bottom. All eleven were carousel slides that vertical scrolling never wakes — and&lt;br&gt;
their URLs still carried &lt;code&gt;&amp;amp;width=30&lt;/code&gt;, which is the theme's placeholder. Nothing was broken. Fix:&lt;br&gt;
never claim a broken image from the DOM; make an actual request for each candidate URL and only&lt;br&gt;
report &lt;code&gt;4xx&lt;/code&gt;/&lt;code&gt;5xx&lt;/code&gt; from the server. On another store that same check confirmed eight genuine 404s, so&lt;br&gt;
it removes fiction without removing findings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Two hundred and forty-eight broken images, including the logo."&lt;/strong&gt; Every image on the site lived on&lt;br&gt;
a separate image host, and every single one came back &lt;code&gt;403&lt;/code&gt; with &lt;code&gt;Cf-Mitigated: challenge&lt;/code&gt; and a&lt;br&gt;
three-kilobyte body, while the main domain answered &lt;code&gt;200&lt;/code&gt;. Cloudflare was challenging &lt;em&gt;my&lt;/em&gt; address,&lt;br&gt;
and an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; request cannot solve a challenge, so all of them failed at once.&lt;/p&gt;

&lt;p&gt;That one has a heuristic worth memorising, because it identifies the class instantly: &lt;strong&gt;if the&lt;br&gt;
company logo is also "broken", it is almost certainly you.&lt;/strong&gt; A live business notices a missing logo&lt;br&gt;
the same day. A hundred per cent failure rate is a property of your connection, not of their site.&lt;br&gt;
The same goes for &lt;code&gt;406&lt;/code&gt; — some servers return it for short &lt;code&gt;User-Agent&lt;/code&gt; strings and &lt;code&gt;200&lt;/code&gt; for a full&lt;br&gt;
Chrome one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"A whole row of products faded to nothing."&lt;/strong&gt; The screenshot showed it twice, and the computed&lt;br&gt;
style backed it up: &lt;code&gt;opacity: 0.01&lt;/code&gt; on the slider wrapper. Which is the exact bug from the first half&lt;br&gt;
of this article, so I nearly sent it. It was false. The theme reveals blocks with&lt;br&gt;
&lt;code&gt;IntersectionObserver&lt;/code&gt;, my script was paging through the document at 320 ms per screen and then&lt;br&gt;
calling &lt;code&gt;scrollIntoView&lt;/code&gt;, which restarted the reveal. At 450 ms per screen with a three-second hold,&lt;br&gt;
every block read &lt;code&gt;opacity: 1&lt;/code&gt;, on three separate pages.&lt;/p&gt;

&lt;p&gt;Note the sting in that: a &lt;em&gt;computed style&lt;/em&gt; is usually the thing that settles an argument, and here it&lt;br&gt;
lied, because I photographed an animation mid-flight. The distinguishing test is the distribution.&lt;br&gt;
Some blocks at 1 and some at 0.01 is their code. All of them faded, or none, is your own scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule underneath all four
&lt;/h2&gt;

&lt;p&gt;Every one of these is the same mistake wearing a different costume: &lt;strong&gt;I measured the environment and&lt;br&gt;
attributed it to the object.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The check is cheap. Take three of your results at random and look at them by eye. If they all point&lt;br&gt;
at the same thing — the same host, the same wrapper, the same status code — you have found a property&lt;br&gt;
of the page's furniture or of your own connection, not a property of the objects you thought you were&lt;br&gt;
measuring. Real defects do not distribute that evenly. A finding that comes back at a hundred per cent&lt;br&gt;
or at zero per cent is a finding about your instrument.&lt;/p&gt;

&lt;p&gt;And before anything goes in front of the person who owns the site: confirm it a second way, and let&lt;br&gt;
the second way be different in kind. Not the same script run twice — a different tool, or a different&lt;br&gt;
angle. Over one week that rule killed roughly two thirds of what my detector produced, and every&lt;br&gt;
single thing that survived it held up.&lt;/p&gt;




&lt;p&gt;If you have an element that is loaded and invisible, paste the &lt;code&gt;whyInvisible&lt;/code&gt; function above into the&lt;br&gt;
console and point it at the element. It is usually the second or third row.&lt;/p&gt;

&lt;p&gt;I do audits like this on real sites, and the repairs that follow.&lt;br&gt;
Portfolio: &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt; ·&lt;br&gt;
Telegram &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; · &lt;code&gt;paladei702@gmail.com&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>css</category>
      <category>debugging</category>
      <category>webdev</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>Your store weighs 1.7 MB and still takes 18 seconds: the number that matters is 364 requests</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Sat, 08 Aug 2026 06:55:02 +0000</pubDate>
      <link>https://dev.to/smirnovartur/your-store-weighs-17-mb-and-still-takes-18-seconds-the-number-that-matters-is-364-requests-6lb</link>
      <guid>https://dev.to/smirnovartur/your-store-weighs-17-mb-and-still-takes-18-seconds-the-number-that-matters-is-364-requests-6lb</guid>
      <description>&lt;p&gt;Page weight is the number everyone checks, and it is the reason this store has been slow for&lt;br&gt;
months without anyone finding out why.&lt;/p&gt;

&lt;p&gt;I measured a Shopify storefront twice, cold cache, an hour apart:&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;visit 1&lt;/th&gt;
&lt;th&gt;visit 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DOMContentLoaded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.2 s&lt;/td&gt;
&lt;td&gt;2.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;load&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18.7 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18.1 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transferred&lt;/td&gt;
&lt;td&gt;2 637 KB&lt;/td&gt;
&lt;td&gt;1 736 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;364&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;343&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;1.7 MB. By weight this is a healthy page — lighter than most of the stores I look at, and lighter&lt;br&gt;
than plenty of pages that finish in under two seconds. It still spends eighteen seconds finishing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the owner does not see it
&lt;/h2&gt;

&lt;p&gt;Look at the first row. The document is parsed and interactive in one to three seconds. The owner&lt;br&gt;
opens the shop, the header is there, the products are there, they scroll, they click, everything&lt;br&gt;
responds. Nothing about that experience says "eighteen seconds".&lt;/p&gt;

&lt;p&gt;The tail is not nothing, though. Until &lt;code&gt;load&lt;/code&gt; fires you are still holding connections open, still&lt;br&gt;
running the browser's networking machinery, still competing for bandwidth on a phone that has very&lt;br&gt;
little of it. Images below the fold arrive late. Anything that hangs off &lt;code&gt;window.onload&lt;/code&gt; — and on a&lt;br&gt;
store with a stack of apps, quite a lot does — is on the far side of that eighteen seconds. The&lt;br&gt;
third-party pixels the owner is paying for fire when the visitor has already left.&lt;/p&gt;

&lt;p&gt;And a visitor on a slow connection is not living in the owner's &lt;code&gt;DOMContentLoaded&lt;/code&gt;. Three hundred&lt;br&gt;
sequential round trips on high-latency mobile is not "a bit slower than desktop", it is a different&lt;br&gt;
page.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why every weight-based check gives it a pass
&lt;/h2&gt;

&lt;p&gt;Because it is not heavy. That is the whole trap.&lt;/p&gt;

&lt;p&gt;The advice that comes back from a weight audit — compress your images, ship smaller JavaScript,&lt;br&gt;
lazy-load the hero — has nothing to bite on here. The scripts total 684 KB, which is unremarkable.&lt;br&gt;
There is no 6 MB PNG. There is no 4 MB logo. Every individual thing on the page is defensible, and&lt;br&gt;
there are three hundred and sixty-four of them.&lt;/p&gt;

&lt;p&gt;Compare with a store I measured the same week, where the weight &lt;em&gt;was&lt;/em&gt; the story: a product page at&lt;br&gt;
8 293 KB across 286 requests, of which 7 011 KB was twenty-seven theme images — and the identical&lt;br&gt;
7 MB showed up on the home page too, because they were photographs inside a dropdown menu that&lt;br&gt;
shipped on every page in the shop. That one is a weight problem, and a weight tool finds it in&lt;br&gt;
thirty seconds.&lt;/p&gt;

&lt;p&gt;The 364-request store is the other class. Same symptom for the customer, opposite diagnosis, and&lt;br&gt;
one of the two diagnoses is invisible to the tool most people reach for.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measure it yourself, in the console, in ten seconds
&lt;/h2&gt;

&lt;p&gt;Open the site in a fresh incognito window with the network panel on and "Disable cache" ticked.&lt;br&gt;
Then paste this:&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;nav&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;navigation&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="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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;requests     &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="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transferred  &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&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;e&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;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferSize&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="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MB&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domContentLoadedEventEnd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;load            &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loadEventEnd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&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;Two numbers matter and most reports only show you one. If &lt;code&gt;load&lt;/code&gt; is many times&lt;br&gt;
&lt;code&gt;domContentLoadedEventEnd&lt;/code&gt;, and the transferred total is unremarkable, you are looking at a request&lt;br&gt;
count problem and no amount of image compression will move it.&lt;/p&gt;

&lt;p&gt;To see where the requests go:&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;by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initiatorType&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;other&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&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="na"&gt;n&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;bytes&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;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferSize&lt;/span&gt;&lt;span class="p"&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;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sort by &lt;code&gt;n&lt;/code&gt;, not by &lt;code&gt;bytes&lt;/code&gt;. The row you are looking for is usually a long list of small files from&lt;br&gt;
a handful of hosts — an app that ships eleven scripts instead of one, an icon set delivered as&lt;br&gt;
individual SVGs, a font family loaded weight by weight, a review widget that fetches every avatar&lt;br&gt;
separately.&lt;/p&gt;

&lt;p&gt;Then the second half, which is the actual fix and which nobody can do for you from the outside:&lt;br&gt;
group the third-party hosts.&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;hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&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;hosts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&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;1&lt;/span&gt;&lt;span class="p"&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;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hosts&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&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="mi"&gt;1&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;15&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every distinct host is a fresh DNS lookup, TCP handshake and TLS negotiation before a single byte of&lt;br&gt;
content moves. On a desktop with a warm connection that is noise. On a phone on a cell network it is&lt;br&gt;
the majority of your eighteen seconds. If one host in that table is serving you forty files for a&lt;br&gt;
feature you do not use, you have found your afternoon's work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that stops you shipping a wrong number
&lt;/h2&gt;

&lt;p&gt;Two of these, both bought the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sum the parts and compare to the whole.&lt;/strong&gt; I once produced a breakdown where CSS came out at&lt;br&gt;
1 061 567 bytes on a page that transferred 1 323 722 bytes in total — CSS was eighty per cent of a&lt;br&gt;
page full of photographs, which is absurd on its face. The cause: images were being pulled in with&lt;br&gt;
&lt;code&gt;&amp;lt;link rel="preload"&amp;gt;&lt;/code&gt;, and my grouping used &lt;code&gt;initiatorType&lt;/code&gt;, which reports those as &lt;code&gt;link&lt;/code&gt;. The same&lt;br&gt;
PNGs were counted once as images and once as stylesheets. If your categories add up to more than the&lt;br&gt;
page, the measurement is wrong in full, not slightly off — go find the item you counted twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure twice or say nothing.&lt;/strong&gt; A single timing run is worth close to nothing. From the same batch&lt;br&gt;
of stores: one gave 20.1 s on the first run and 7.7 s on the second. Another gave 13.3 s, then 0.0 s&lt;br&gt;
with &lt;code&gt;load&lt;/code&gt; at 629 ms over 409 KB. A third gave 9.6 s, then 0.0 s. Three findings that would have&lt;br&gt;
been three confident, wrong emails.&lt;/p&gt;

&lt;p&gt;The store at the top of this article is in the article precisely because it survived: 18.7 s and&lt;br&gt;
18.1 s, 364 and 343 requests, an hour apart. Numbers that repeat are findings. Numbers that appear&lt;br&gt;
once are hypotheses, and the coin lands heads about half the time.&lt;/p&gt;

&lt;p&gt;There is a third failure mode worth naming, because it looks like a finding and it is not yours:&lt;br&gt;
I measured the same home page twice and got 10 510 KB / 453 requests and then 4 162 KB / 492&lt;br&gt;
requests. Two visits, one page, six megabytes apart. The difference was an embedded social feed&lt;br&gt;
pulling roughly 2.9 MB of images from someone else's CDN, and what it served varied per visit. The&lt;br&gt;
shop's own pages were light. If you are reporting a number to a client, know whose bytes you are&lt;br&gt;
counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would actually do with this store
&lt;/h2&gt;

&lt;p&gt;In order, and only in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Count requests per host and per feature.&lt;/strong&gt; Not per file type — per feature. "The reviews app is
64 requests" is an actionable sentence. "Images are 210 requests" is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete before optimising.&lt;/strong&gt; On most storefronts there is at least one app that was installed for
a campaign two years ago and never removed. It costs zero engineering hours to remove and it is
usually worth more than every other item on the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle what is left.&lt;/strong&gt; Icons into a sprite or inline SVG, fonts to the two weights you actually
render, a script that ships eleven files into one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move what survives off the critical path&lt;/strong&gt; so it is not holding &lt;code&gt;load&lt;/code&gt; open — &lt;code&gt;defer&lt;/code&gt;, or load
on interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-measure cold, twice&lt;/strong&gt;, and compare &lt;code&gt;load&lt;/code&gt; to the two runs above. If it did not move in both
runs, it did not move.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Eighteen seconds does not become one second. It becomes three or four, and the pixels the owner is&lt;br&gt;
paying for start firing while the customer is still on the page.&lt;/p&gt;




&lt;p&gt;If your own numbers look like the table at the top — small page, long tail — the two console&lt;br&gt;
snippets above will tell you which host is doing it, and that is genuinely most of the work.&lt;/p&gt;

&lt;p&gt;I do this kind of measurement and the repair that follows, mostly for storefronts and product&lt;br&gt;
sites. Portfolio: &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt; ·&lt;br&gt;
Telegram &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; · &lt;code&gt;paladei702@gmail.com&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>shopify</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>A 4 MB logo drawn at 36 pixels: the two files that skipped the image optimiser</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:41:56 +0000</pubDate>
      <link>https://dev.to/smirnovartur/a-4-mb-logo-drawn-at-36-pixels-the-two-files-that-skipped-the-image-optimiser-1g6o</link>
      <guid>https://dev.to/smirnovartur/a-4-mb-logo-drawn-at-36-pixels-the-two-files-that-skipped-the-image-optimiser-1g6o</guid>
      <description>&lt;p&gt;A landing page I measured last week sends &lt;strong&gt;11.7 MB to a phone&lt;/strong&gt;. Two files account for &lt;strong&gt;11.2 MB&lt;/strong&gt; of it:&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;weight&lt;/th&gt;
&lt;th&gt;how it appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;how_it_works.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.9 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;one enormous raster image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logo.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.1 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a 2048 px file, drawn on screen at &lt;strong&gt;36 px&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four megabytes of logo to paint an icon the size of a fingernail.&lt;/p&gt;

&lt;p&gt;The part that makes this worth writing about is not the number. It is that &lt;strong&gt;the rest of the page was done correctly.&lt;/strong&gt; The before/after sample images on the same page go through the framework's image component and arrive on mobile at &lt;strong&gt;71 KB and 54 KB&lt;/strong&gt;, properly narrowed to &lt;code&gt;w=640&lt;/code&gt;. Someone set that up, and it works.&lt;/p&gt;

&lt;p&gt;So the site does not have a person who doesn't care about images. It has two &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;h2&gt;
  
  
  The optimiser is opt-in, and nothing tells you when you opted out
&lt;/h2&gt;

&lt;p&gt;Every modern framework ships an image pipeline — &lt;code&gt;next/image&lt;/code&gt;, Nuxt Image, Astro's &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt;, Gatsby, whatever your stack calls it. Point it at a file and you get resizing, format negotiation, a &lt;code&gt;srcset&lt;/code&gt;, lazy loading, the lot.&lt;/p&gt;

&lt;p&gt;Point &lt;em&gt;nothing&lt;/em&gt; at it and you get the original file, at original size, forever.&lt;/p&gt;

&lt;p&gt;That is the whole bug. The two heavy files were plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags. They render identically to the optimised ones. Same layout, same crop, same visual result at every breakpoint. There is no warning, no build error, no lint rule firing by default, and no visual difference to catch in review — because at 36 px on screen, a 2048 px source and a 72 px source look exactly the same.&lt;/p&gt;

&lt;p&gt;It usually happens the same way, too. The optimised images are the ones that arrived in the content pipeline — product shots, gallery items, anything in a loop. The unoptimised ones are the &lt;strong&gt;furniture&lt;/strong&gt;: a logo dropped in during the first hour of the project, a diagram exported from Figma and dragged into the hero. They get placed once, early, by someone who is not yet thinking about performance, and then they are never touched again because they are never wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check yours in about a minute
&lt;/h2&gt;

&lt;p&gt;Open DevTools, Network, filter to Img, disable cache, reload. Sort by size, largest first.&lt;/p&gt;

&lt;p&gt;Then look at the &lt;strong&gt;URLs&lt;/strong&gt;, not the file names. Optimised assets carry the pipeline's signature — &lt;code&gt;/_next/image?url=…&amp;amp;w=640&amp;amp;q=75&lt;/code&gt;, &lt;code&gt;/_ipx/&lt;/code&gt;, &lt;code&gt;/_vercel/image&lt;/code&gt;, a CDN transform path, something with dimensions in the query string. Unoptimised ones are just &lt;code&gt;/logo.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything at the top of that list without a transform in its URL is a file that skipped the pipeline.&lt;/strong&gt; That's your answer, and it takes longer to read this paragraph than to run it.&lt;/p&gt;

&lt;p&gt;Two follow-ups worth thirty seconds each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compare rendered size to natural size.&lt;/strong&gt; In the Elements panel, hover the image: Chrome shows both. A 2048 px file in a 36 px box is a 57× overshoot in each dimension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A logo almost never needs to be a raster at all.&lt;/strong&gt; Ours is an icon: SVG, or a 72 px PNG at 2× if the mark is too painterly to vectorise. Either one is measured in kilobytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest part: quote weight, not time
&lt;/h2&gt;

&lt;p&gt;I ran the first cold visit at &lt;strong&gt;32 seconds&lt;/strong&gt;. The second, with a warm cache, was &lt;strong&gt;under two&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both are real, and quoting either one alone is misleading. So the correct thing to say is either both numbers with the conditions attached, or neither — and to build the argument on &lt;strong&gt;weight&lt;/strong&gt;, which does not move: 11.7 MB stays 11.7 MB regardless of whose connection measures it.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds when you're telling someone else about their site. Load time is a property of the path between two machines; you measured your own network, not their customer's. Weight is a property of the page itself. One of those you own, the other you're borrowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth an hour of anyone's time
&lt;/h2&gt;

&lt;p&gt;The site I measured belongs to someone about to buy TikTok and Instagram ads. That traffic is phones on cellular with no patience, and every ad dollar was going to buy a visitor who leaves before the page assembles.&lt;/p&gt;

&lt;p&gt;Routing two files through the pipeline that was already set up, giving the logo an SVG, and slicing the tall diagram into sections turns 11.7 MB into under a megabyte — &lt;strong&gt;without a single change to the design.&lt;/strong&gt; Nothing moves, nothing is redrawn, nothing needs approval from whoever owns the brand.&lt;/p&gt;

&lt;p&gt;That's the shape of the best performance work: not a redesign, not a rewrite, just finding the things that quietly bypassed a system you already built.&lt;/p&gt;




&lt;p&gt;I build browser tools where measurement is the job — parametric product configurators and engineering calculators, where the geometry and the costing come from the same parameters so they can't quietly disagree. Work at &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt;, and I'm on Telegram at &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; or at &lt;a href="mailto:paladei702@gmail.com"&gt;paladei702@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run the URL check above and find something at the top of the list without a transform in it, I'd be curious what it was. My money is on a logo.&lt;/p&gt;

</description>
      <category>webperfnextjs</category>
      <category>webperf</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your page weight is not a number, it's a range: 14 MB one visit, 55 MB the next</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:32:20 +0000</pubDate>
      <link>https://dev.to/smirnovartur/your-page-weight-is-not-a-number-its-a-range-14-mb-one-visit-55-mb-the-next-3ol</link>
      <guid>https://dev.to/smirnovartur/your-page-weight-is-not-a-number-its-a-range-14-mb-one-visit-55-mb-the-next-3ol</guid>
      <description>&lt;p&gt;I measure other people's pages and write to the owners about what I find, so I care a lot about which of my numbers are safe to quote. My working rule for a long time was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Load time is a range — it depends on the network. &lt;strong&gt;Page weight is stable.&lt;/strong&gt; Build the argument on weight.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That rule is mostly right, and last week it was wrong in a way I want to write down, because the failure is more interesting than the rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same page, two visits, ten minutes apart
&lt;/h2&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;visit 1&lt;/th&gt;
&lt;th&gt;visit 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;total transferred&lt;/td&gt;
&lt;td&gt;14,486 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55,245 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;requests&lt;/td&gt;
&lt;td&gt;533&lt;/td&gt;
&lt;td&gt;638&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;first-party only&lt;/td&gt;
&lt;td&gt;6,499 KB&lt;/td&gt;
&lt;td&gt;4,388 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time to ready&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25.3 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.5 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nearly &lt;strong&gt;four times the weight&lt;/strong&gt; — and it got &lt;em&gt;faster&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Both numbers are real. Nothing was cached differently on purpose, nothing changed on their server. If I had measured once and written a message, I'd have quoted either 14 MB or 55 MB with equal confidence, and been equally wrong either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually happening
&lt;/h2&gt;

&lt;p&gt;An Instagram feed widget in the footer. On the second visit it pulled &lt;strong&gt;eight or more videos, 2.3–4.0 MB each&lt;/strong&gt;, from &lt;code&gt;cdninstagram.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How many it manages to fetch depends on how quickly the run reaches the footer and how long it lingers — which is a property of &lt;em&gt;my&lt;/em&gt; measurement, not of their page. And the inversion makes sense once you see it: on the faster visit the page became ready sooner, so the footer widget got more time to keep pulling videos before I stopped counting. &lt;strong&gt;Speed caused the weight.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Note the third row, too. Their own first-party bytes moved from 6,499 KB to 4,388 KB — a third of a swing, from a source I haven't fully explained. Even the "stable" half of the measurement is only stable-ish.&lt;/p&gt;

&lt;p&gt;So the amended rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Measure weight twice.&lt;/strong&gt; If the second run differs by more than about 1.5×, stop measuring the page and go find the third-party widget. Then write about the widget, not about "page weight."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And: &lt;strong&gt;never fold third-party video into "the weight of their site."&lt;/strong&gt; Those bytes are real and worth telling someone about, but they belong to an embed, and putting them in a single total lets one wrong assumption discredit every other number in the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other blind spot: the heaviest file is often not yours
&lt;/h2&gt;

&lt;p&gt;A related case, from a different kind of site — a product that audits pages for SEO and speed. I ran its own home page through the same lens.&lt;/p&gt;

&lt;p&gt;The heaviest single file was &lt;strong&gt;Google Tag Manager: 169,522 bytes over the wire, 498,110 decompressed.&lt;/strong&gt; For comparison, on the same page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;their own JS bundle: &lt;strong&gt;116,026&lt;/strong&gt; bytes over the wire&lt;/li&gt;
&lt;li&gt;their CSS: &lt;strong&gt;81,318&lt;/strong&gt; over the wire&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The analytics tag is bigger than the application. On a page-speed product.&lt;/p&gt;

&lt;p&gt;I don't think anyone was careless. GTM is pasted in once, near the beginning, by whoever set up analytics, and it never appears in a bundle report again — it isn't in your &lt;code&gt;package.json&lt;/code&gt;, your bundler never sees it, and your build output never mentions it. It is invisible to exactly the tools you'd use to look for it, including, in that case, the site's own product.&lt;/p&gt;

&lt;p&gt;Both stories are the same story: &lt;strong&gt;the things that distort a page most are the things outside the boundary your tooling draws.&lt;/strong&gt; A bundler measures your bundle. A framework's image pipeline measures the images you handed it. An embed measures nothing at all and answers to no one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually do now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two cold runs, always.&lt;/strong&gt; One run is an anecdote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split first-party from third-party in the report&lt;/strong&gt;, never a single grand total. The split is usually the finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the two runs disagree by more than 1.5×, don't quote a weight at all&lt;/strong&gt; — name the widget and quote &lt;em&gt;its&lt;/em&gt; files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort by transfer size and read the domains&lt;/strong&gt;, not just the file names. If the top entry isn't served from the site's own domain, that's the headline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quote decompressed size next to over-the-wire size&lt;/strong&gt; for scripts. 169 KB sounds survivable; 498 KB of JavaScript to parse on the main thread is a different sentence about the same file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is sophisticated. It is just the difference between a number you can defend and a number that collapses the first time the recipient reloads their own page and sees something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Because an unsolicited measurement is worth exactly as much as its credibility. Send someone "your site is 55 MB", have them check and see 14, and you have not made a small error — you have converted yourself into spam, and every accurate thing you also said goes in the bin with it.&lt;/p&gt;

&lt;p&gt;The two-run rule costs one extra page load. It is the cheapest insurance I know of.&lt;/p&gt;




&lt;p&gt;I build browser tools where this kind of measurement is the job — parametric product configurators and engineering calculators, where the geometry and the costing come from the same parameters so they can't quietly disagree. Work at &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt;, and I'm on Telegram at &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; or at &lt;a href="mailto:paladei702@gmail.com"&gt;paladei702@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have a page where two cold runs disagree by more than 1.5×, I'd genuinely like to see it. My guess is there's a video embed in your footer.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your dropdown menu is 7 MB, and it ships on every page you have</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:31:47 +0000</pubDate>
      <link>https://dev.to/smirnovartur/your-dropdown-menu-is-7-mb-and-it-ships-on-every-page-you-have-284c</link>
      <guid>https://dev.to/smirnovartur/your-dropdown-menu-is-7-mb-and-it-ships-on-every-page-you-have-284c</guid>
      <description>&lt;p&gt;A product page on a jewellery store weighs &lt;strong&gt;8,293 KB&lt;/strong&gt;. That is bad but unremarkable — plenty of stores are heavy, and the usual explanation is "it's a product page, it has photos."&lt;/p&gt;

&lt;p&gt;Then I measured a second page on the same store, and the diagnosis changed completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7,011 KB of that product page is 27 theme images. Twenty-five of them are photographs in the dropdown navigation menu. And the same 7 MB is on the home page.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Identical. Not similar — the same set of files, the same bytes, on a page that has nothing to do with the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the second page is the whole finding
&lt;/h2&gt;

&lt;p&gt;One page measurement can only tell you a page is heavy. It cannot tell you &lt;em&gt;whose fault it is&lt;/em&gt;, and that distinction is the difference between a useful message and an annoying one.&lt;/p&gt;

&lt;p&gt;Three explanations fit a single heavy page equally well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;this particular page has a lot on it,&lt;/li&gt;
&lt;li&gt;the platform or theme is heavy by default,&lt;/li&gt;
&lt;li&gt;something shared is riding along on every page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Measure a second, unrelated page on the same site and the three separate instantly. &lt;strong&gt;Whatever is identical between two unrelated pages is shared furniture. Whatever differs is the page.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here the 7 MB was identical. So it isn't the product page, and it isn't a general platform tax either — it's a menu. Every visitor downloads twenty-five navigation photographs to look at a checkout page, a policy page, a 404.&lt;/p&gt;

&lt;p&gt;That reframing is what makes it fixable. "Your product page is 8 MB" invites a shrug. "Your dropdown menu is 7 MB and it ships on every page" is a specific object with an owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism, and why the browser can't save you
&lt;/h2&gt;

&lt;p&gt;Three properties, and it takes all three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The files are around &lt;strong&gt;2400 px&lt;/strong&gt; wide, sitting in slots of roughly &lt;strong&gt;300 px&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;They carry &lt;code&gt;loading="eager"&lt;/code&gt;, so they're fetched immediately rather than when the menu opens.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;None of them has a &lt;code&gt;srcset&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the trap. A modern browser is perfectly willing to fetch a smaller variant — but only if you offer one. With no &lt;code&gt;srcset&lt;/code&gt;, there is no smaller variant in existence, and the browser is obliged to take the 2400 px file to paint 300 px of it. This is not the browser being conservative. It has nothing else to choose from.&lt;/p&gt;

&lt;p&gt;The same pattern, on a different store: &lt;strong&gt;25 images larger than their slots, 5,480 KB total, and &lt;code&gt;srcset&lt;/code&gt; on none of the 25.&lt;/strong&gt; The banner is 3609×5414 — 19.5 megapixels, 1,432 KB — in a 340×460 slot. Five circular category thumbnails are 2380×2380, 210–291 KB each, in 74×74 slots. That's &lt;strong&gt;32× wider than needed&lt;/strong&gt;, per circle, for decoration.&lt;/p&gt;

&lt;p&gt;Nobody chose any of this. Theme images get uploaded once at whatever size the source file happened to be, through an admin panel that accepts them without comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do this on your own store, it takes two minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open your &lt;strong&gt;home page&lt;/strong&gt;, DevTools → Network → Img, disable cache, reload. Note the total.&lt;/li&gt;
&lt;li&gt;Open a &lt;strong&gt;product page&lt;/strong&gt;, or any page as unlike the home page as you can find. Note the total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subtract.&lt;/strong&gt; The overlap is what every visitor pays on every page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then, for anything big in the overlap, check two things in the Elements panel: the natural size against the rendered size, and whether the tag has a &lt;code&gt;srcset&lt;/code&gt; at all. Overshoot plus no &lt;code&gt;srcset&lt;/code&gt; is a file that literally cannot be delivered smaller.&lt;/p&gt;

&lt;p&gt;Sort the overlap by size and fix downward. On both stores above, the top five files were most of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I threw away, because it matters
&lt;/h2&gt;

&lt;p&gt;I ran a detector across a batch of these stores, and roughly a third of what it flagged was wrong. Since the whole point is to tell owners true things, here is what fooled it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Six buttons have run off the right edge."&lt;/strong&gt; It was a carousel. Off-screen slides are how carousels work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"A row of products is faded out at &lt;code&gt;opacity: 0.01&lt;/code&gt;."&lt;/strong&gt; That was a scroll-triggered reveal, caught mid-animation because my script scrolled faster than a human ever would.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"This product card has no price."&lt;/strong&gt; One store had &lt;strong&gt;209&lt;/strong&gt; price elements on the page. My regex simply didn't recognise the Indian digit-grouping format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Overflow of 175 px at 390 px wide."&lt;/strong&gt; At 320 px the overflow was &lt;strong&gt;zero&lt;/strong&gt; — which is impossible for a genuine layout break, since narrower is always worse. The culprit was a hidden cart panel parked off-screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule I now use: &lt;strong&gt;a finding that gets better as the viewport gets narrower is not a layout break&lt;/strong&gt;, and a signal from code is never the same thing as a working feature. Reproduce twice, and look at a screenshot before you believe any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general version
&lt;/h2&gt;

&lt;p&gt;Most "your site is slow" advice fails because it can't assign responsibility. Owners on hosted platforms have heard it all and have a ready and often correct answer: &lt;em&gt;that's just how the platform is.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The two-page subtraction is the cheapest way past that. It costs two page loads, needs no access to their account, and it converts an opinion into an object: this menu, these twenty-five files, this many megabytes, on every page you have.&lt;/p&gt;




&lt;p&gt;I build browser tools where this kind of measurement is the job — parametric product configurators and engineering calculators, where the geometry and the costing come from the same parameters so they can't quietly disagree. Work at &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt;, and I'm on Telegram at &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; or at &lt;a href="mailto:paladei702@gmail.com"&gt;paladei702@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run the subtraction on your own store, I'd like to hear what was in the overlap. So far mine is winning at 7 MB of menu.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>webperf</category>
      <category>shopify</category>
    </item>
    <item>
      <title>Chrome is downloading your 22 MB mp4 while the 3.8 MB webm sits one line below</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:19:42 +0000</pubDate>
      <link>https://dev.to/smirnovartur/chrome-is-downloading-your-22-mb-mp4-while-the-38-mb-webm-sits-one-line-below-3n51</link>
      <guid>https://dev.to/smirnovartur/chrome-is-downloading-your-22-mb-mp4-while-the-38-mb-webm-sits-one-line-below-3n51</guid>
      <description>&lt;p&gt;Here is a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tag I found on a live production site. Read it and see if anything looks wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;autoplay&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt; &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;preload=&lt;/span&gt;&lt;span class="s"&gt;"auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/videos/bg-loop.mp4"&lt;/span&gt;  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/videos/bg-loop.webm"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/webm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing looks wrong. That is the shape everyone writes. It is also shipping &lt;strong&gt;22,730,246 bytes&lt;/strong&gt; to every Chrome visitor when the file on the next line is &lt;strong&gt;3,807,102 bytes&lt;/strong&gt; of the same clip.&lt;/p&gt;

&lt;p&gt;Six times heavier. One line apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;The rule is one sentence, and it is not "pick the best one":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The browser plays the first &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; whose type it can decode. It never looks at the rest.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Chrome decodes mp4. So Chrome reads line one, says yes, and stops. The webm underneath is never evaluated — not compared, not considered, not even fetched. It exists purely to make the markup look thorough.&lt;/p&gt;

&lt;p&gt;The habit comes from a real era: for years webm support was patchy and mp4 was the safe fallback, so mp4-first was correct defensive markup. That era ended. The habit didn't, and nothing in the page complains, because &lt;strong&gt;nothing is broken.&lt;/strong&gt; The video plays. The layout is fine. There is no console error, no failed request, no red anything. The only symptom is the bandwidth bill and the phone that gets warm.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;preload="auto"&lt;/code&gt; on that tag means it isn't even deferred — the browser starts pulling all 22 MB immediately, before the visitor has decided to stay.&lt;/p&gt;

&lt;p&gt;The fix is to swap two lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  It travels in packs
&lt;/h2&gt;

&lt;p&gt;I found the same shape on a second, unrelated site the same week, with an extra layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/videos/banner-logo.mov"&lt;/span&gt;  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/videos/banner-logo.webm"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/webm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the first line twice. The file is &lt;code&gt;.mov&lt;/code&gt;, the declared type is &lt;code&gt;video/mp4&lt;/code&gt;, and the server answers with &lt;code&gt;Content-Type: video/quicktime&lt;/code&gt;. Three different opinions about one file, in one line.&lt;/p&gt;

&lt;p&gt;That site also served two other QuickTime files — 3,374,781 and roughly 1,267 KB — &lt;strong&gt;with no alternative source at all.&lt;/strong&gt; Not mis-ordered; simply the only option, in a container that has no business being a web delivery format.&lt;/p&gt;

&lt;p&gt;Two independent sites, one week. This isn't an anecdote about one careless team, it's a default that survives review because it reads as correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own in thirty seconds
&lt;/h2&gt;

&lt;p&gt;Don't read your markup — markup is what you &lt;em&gt;meant&lt;/em&gt;. Open devtools, Network tab, filter to Media, hard-reload, and read &lt;strong&gt;which file the browser actually fetched&lt;/strong&gt; and how many bytes it took.&lt;/p&gt;

&lt;p&gt;Then compare that against what's in the HTML. If the fetched file is not the smallest one you offered, your &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; order is upside down.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to not overstate this, which matters if you're telling someone else
&lt;/h2&gt;

&lt;p&gt;I audit other people's sites and write to the owners, so a wrong number costs me the entire conversation. Three ways I have watched video-weight claims fall apart under checking — all three are mistakes I had to catch in my own measurements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never sum the sources.&lt;/strong&gt; The browser fetches exactly one. Adding the mp4 and the webm together to produce a scary total describes a download that has never happened to anyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;preload="metadata"&lt;/code&gt; without &lt;code&gt;autoplay&lt;/code&gt; means the body is never fetched.&lt;/strong&gt; You can &lt;code&gt;curl&lt;/code&gt; that file and get 30 MB, and the number is real and completely irrelevant — no visitor downloads it until they press play. Before quoting a video's weight, check the tag: it counts if there's &lt;code&gt;autoplay&lt;/code&gt;, or &lt;code&gt;preload="auto"&lt;/code&gt;. Not otherwise. I had ten weight findings once and lost six to this check and its image equivalent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The image version is &lt;code&gt;srcset&lt;/code&gt;.&lt;/strong&gt; Fetch an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;'s &lt;code&gt;src&lt;/code&gt; with a script and you get the full-size original; the browser, meanwhile, picked a 400px variant. If the tag has &lt;code&gt;srcset&lt;/code&gt;, your measured weight is fiction.&lt;/p&gt;

&lt;p&gt;What survives all three is the good stuff: a single &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; with &lt;code&gt;autoplay&lt;/code&gt;, an image with no &lt;code&gt;srcset&lt;/code&gt;, and any JS or CSS file — those have no responsive negotiation at all, which is exactly why findings in code are the most reliable ones you can make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;The interesting bugs in a page are rarely errors. An error announces itself; someone eventually fixes it. What persists for years is &lt;strong&gt;markup that is syntactically perfect and semantically backwards&lt;/strong&gt; — a correct list in the wrong order, a correct type attribute on the wrong file, a correct preload hint on something that shouldn't preload.&lt;/p&gt;

&lt;p&gt;Nothing in your toolchain flags any of it. A linter sees valid HTML. A build sees valid HTML. Lighthouse will tell you the page is heavy but not that the lighter file was sitting right there, unused, one line below.&lt;/p&gt;

&lt;p&gt;The only thing that catches this class of bug is opening the network panel and comparing what you &lt;em&gt;offered&lt;/em&gt; with what was &lt;em&gt;taken&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;I build browser tools where this kind of measurement is the job — parametric product configurators and engineering calculators, where the geometry and the costing come from the same parameters so they can't quietly disagree. Work at &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt;, and I'm on Telegram at &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; or at &lt;a href="mailto:paladei702@gmail.com"&gt;paladei702@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you check your own and the order was backwards, I'd like to know — I have two cases and I suspect the true rate is embarrassing.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>html</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your best line is in your meta description, and nobody reads it</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:16:29 +0000</pubDate>
      <link>https://dev.to/smirnovartur/your-best-line-is-in-your-meta-description-and-nobody-reads-it-4jnl</link>
      <guid>https://dev.to/smirnovartur/your-best-line-is-in-your-meta-description-and-nobody-reads-it-4jnl</guid>
      <description>&lt;p&gt;I audit strangers' websites — I open a page, measure what it ships, and write to the owner about what I found. Somewhere around the fourth one I stopped noticing it as a coincidence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The single best sentence about the business was in the &lt;code&gt;meta description&lt;/code&gt;. Not on the page. Not in the &lt;code&gt;h1&lt;/code&gt;. In a tag nobody looks at.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Four sites in a row. Different countries, different industries, different people. Same shape every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens, and it isn't carelessness
&lt;/h2&gt;

&lt;p&gt;The two lines get written at different moments, by different versions of you.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;h1&lt;/code&gt; gets written &lt;strong&gt;early&lt;/strong&gt;, while you are still nervous about being taken seriously. So it comes out sounding like the category: &lt;em&gt;Make Smarter Decisions&lt;/em&gt;, &lt;em&gt;Modern Solutions for Modern Teams&lt;/em&gt;, &lt;em&gt;Your Partner in X&lt;/em&gt;. You are writing for an imagined investor.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;meta description&lt;/code&gt; gets written &lt;strong&gt;late&lt;/strong&gt;, usually because a checklist told you the field was empty, usually in one pass, usually five minutes before you stop caring. Nobody is watching. So you just say what the thing is.&lt;/p&gt;

&lt;p&gt;That's the whole mechanism. The description is honest because it was written when you'd stopped performing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A social analytics tool.&lt;/strong&gt; The &lt;code&gt;h1&lt;/code&gt;: &lt;em&gt;Make Smarter Social Media Decisions.&lt;/em&gt; Fine, and interchangeable with two hundred other tools. The description named two of its actual metrics — invented vocabulary, specific to that product, the kind of words a user repeats back to you. The words that make the product &lt;em&gt;itself&lt;/em&gt; were sitting in the tag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An AI portrait generator.&lt;/strong&gt; Every competitor in that market runs a subscription. The description contained four words: &lt;strong&gt;pay once, no subscription.&lt;/strong&gt; That is not a feature, it is the entire reason to choose them over anyone else. It appeared nowhere on the visible page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An electronics retailer.&lt;/strong&gt; Their description mentioned a payment arrangement specific to their country — a workplace scheme that lets people buy against payroll. No competitor offered it. It was the one genuinely unfalsifiable difference they had, and it lived in the tag. Meanwhile the browser tab on their home page read &lt;strong&gt;"Home"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A children's activity centre — the reverse case, and my favourite.&lt;/strong&gt; Here the &lt;em&gt;inner&lt;/em&gt; pages were sharper than the entrance. A single activity page named the state it operates in. The home page didn't — and the town shares its name with a far more famous town elsewhere. So the page most people land on was the one page that failed to say where on earth it was.&lt;/p&gt;

&lt;p&gt;That last one is why I don't think this is a rule about descriptions. It's a rule about &lt;strong&gt;where attention goes&lt;/strong&gt;: the page you rewrite most is the page you overthink most, and the pages you never revisit keep telling the truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check, and it takes thirty seconds
&lt;/h2&gt;

&lt;p&gt;Open your own site. View source, or just open devtools:&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="nx"&gt;title&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;h1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&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;meta[name="description"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put the three next to each other and ask one question: &lt;strong&gt;which of them would make a stranger understand what you sell?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is the third one, you already have your headline. You wrote it. You just filed it somewhere no human being reads.&lt;/p&gt;

&lt;p&gt;Then do the same for two or three inner pages, because of the fourth case above. Compare what the deepest page claims about you with what the front door claims. The gap is usually the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;It isn't SEO advice. I'm not telling you to tune description length, or that this moves rankings — I haven't measured that and I'm not going to imply I have. Search engines frequently rewrite the description they display anyway.&lt;/p&gt;

&lt;p&gt;The claim is narrower and, I think, more useful: &lt;strong&gt;the copy problem you think you have is often not a writing problem but a filing problem.&lt;/strong&gt; The sentence exists. It's in the repository. It's just in the wrong field.&lt;/p&gt;

&lt;p&gt;One more thing worth checking while you're in there, since it costs nothing: how many &lt;code&gt;h1&lt;/code&gt; elements the page actually has. On one of these four the answer was &lt;strong&gt;nine&lt;/strong&gt;. That is not a ranking catastrophe, but it does mean the page has no single most important statement — which, if you've read this far, is exactly the problem we started with.&lt;/p&gt;




&lt;p&gt;I build browser tools where the same discipline applies to numbers instead of sentences — parametric product configurators and engineering calculators, where the geometry and the costing are computed from the same parameters so they can't quietly disagree. Work at &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt;, and I'm on Telegram at &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; or at &lt;a href="mailto:paladei702@gmail.com"&gt;paladei702@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run the three-line check on your own site, I'd like to hear whether the description won. My guess is it wins more often than anyone expects, and I'd like a bigger sample than four.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Why your Wix site feels slow: 4.9 MB of HTML before a single image, and which half you can actually fix</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:07:06 +0000</pubDate>
      <link>https://dev.to/smirnovartur/why-your-wix-site-feels-slow-49-mb-of-html-before-a-single-image-and-which-half-you-can-actually-1o8o</link>
      <guid>https://dev.to/smirnovartur/why-your-wix-site-feels-slow-49-mb-of-html-before-a-single-image-and-which-half-you-can-actually-1o8o</guid>
      <description>&lt;p&gt;A store owner posted that his Wix site took about six seconds before it responded to anything. He had already done the obvious: converted images to webp, shrunk them to size. It changed nothing. His question was the honest version of a question a lot of people have:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Once you have already made those changes, and the site is still slow — is there anything else we can do? Or just suffer?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three days, no replies. And the replies he &lt;em&gt;would&lt;/em&gt; eventually get were predictable: compress your images. Which, in his case, was both wrong and slightly insulting, because he'd done it.&lt;/p&gt;

&lt;p&gt;The interesting thing is that this question is answerable — precisely, with a number — and almost nobody answers it, because doing so requires an experiment rather than an opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: the images really were fine
&lt;/h2&gt;

&lt;p&gt;Before looking for a culprit I checked the thing he said he'd already fixed, because the worst outcome here is telling someone to keep digging in a hole that's finished.&lt;/p&gt;

&lt;p&gt;Of 108 media references on the home page, images were being requested at &lt;strong&gt;147×147, 160×160 and 162×162 pixels&lt;/strong&gt;, &lt;code&gt;q_80&lt;/code&gt;, in &lt;strong&gt;webp&lt;/strong&gt; — exactly the size of the slots they sit in, with no headroom. There was not one oversized image on the page. His image work was done and continuing it would have been a waste of his weekend.&lt;/p&gt;

&lt;p&gt;(One exception: the &lt;code&gt;og:image&lt;/code&gt; was requested at 2500×2500. That is a social-preview asset, not something a visitor waits on.)&lt;/p&gt;

&lt;p&gt;So the images were a dead end, which meant the weight was somewhere people don't usually look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTML document is 4.9 MB
&lt;/h2&gt;

&lt;p&gt;Not the page. The &lt;strong&gt;document&lt;/strong&gt; — before a single image, font, or external script.&lt;/p&gt;

&lt;p&gt;Over the wire it arrives compressed at 1,230 KB. Decompressed, the thing the browser has to parse is &lt;strong&gt;4,888 KB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I ran it cold three times, bypassing cache: &lt;strong&gt;1,260,005 / 1,259,977 / 1,259,993 bytes.&lt;/strong&gt; A spread of 28 bytes across three runs. That is not a measurement artefact; that number is real and stable, and you can build an argument on it.&lt;/p&gt;

&lt;p&gt;What is inside those 4,888 KB:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;part&lt;/th&gt;
&lt;th&gt;size&lt;/th&gt;
&lt;th&gt;share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1,579 KB&lt;/td&gt;
&lt;td&gt;32.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inline &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; — &lt;strong&gt;88 separate blocks&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;1,291 KB&lt;/td&gt;
&lt;td&gt;26.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the actual markup&lt;/td&gt;
&lt;td&gt;2,017 KB&lt;/td&gt;
&lt;td&gt;41.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And inside the scripts, two JSON blobs that the browser must parse &lt;strong&gt;on the main thread&lt;/strong&gt; before the page becomes interactive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;wix-warmup-data&lt;/code&gt; — &lt;strong&gt;1,193 KB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wix-viewer-model&lt;/code&gt; — &lt;strong&gt;238 KB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Breaking down the first one by key:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;size&lt;/th&gt;
&lt;th&gt;what it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;platform.ssrPropsUpdates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;939.7 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;serialized props for &lt;em&gt;every element on the page&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appsWarmupData.dataBinding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;197.3 KB&lt;/td&gt;
&lt;td&gt;CMS collections bound to this page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;platform.ssrStyleUpdates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;50.5 KB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15.4 KB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nearly a megabyte of JSON describing the props of every component, shipped inline in the document, parsed synchronously before anything responds. That is where the six seconds went — not into his images.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that makes this usable: the control
&lt;/h2&gt;

&lt;p&gt;Here is where most performance write-ups stop, and where they stay useless. Because the obvious response to everything above is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Fine — but that's the platform. I didn't write any of that. What am I supposed to do, rebuild the site?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That response is reasonable and it deserves an actual answer, not a shrug. So I measured &lt;strong&gt;two more pages on the same platform, in the same account, on the same day.&lt;/strong&gt; Same builder, same infrastructure, same everything — the only variable is how much is on the page.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;page&lt;/th&gt;
&lt;th&gt;document&lt;/th&gt;
&lt;th&gt;warm-up blob&lt;/th&gt;
&lt;th&gt;&lt;code&gt;ssrPropsUpdates&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;home&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,888 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,193 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;940 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a gift-card page&lt;/td&gt;
&lt;td&gt;2,642 KB&lt;/td&gt;
&lt;td&gt;5.5 KB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a near-empty page&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,855 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40.7 KB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two conclusions, and they point at different people:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. About 1.9 MB is an irreducible platform tax.&lt;/strong&gt; His own nearly-empty page — a page with essentially nothing on it — still ships 1,855 KB. He was right, and it's proven by his own site rather than by my opinion. No optimisation he performs will remove that floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The home page adds roughly 3.0 MB on top of the floor, and that part is its own construction.&lt;/strong&gt; The 940 KB &lt;code&gt;ssrPropsUpdates&lt;/code&gt; block exists &lt;em&gt;only&lt;/em&gt; on the home page — it is literally 0 KB on both of the others. It isn't emitted by the platform as a constant; it's generated in proportion to the number of components on that particular page.&lt;/p&gt;

&lt;p&gt;So the answer to "or just suffer?" is &lt;strong&gt;both, in a ratio&lt;/strong&gt;: about 40% of the weight is a floor he cannot touch, and about 60% is his page's own composition, which he can. That is a completely different conversation from "your platform is bloated" &lt;em&gt;and&lt;/em&gt; from "compress your images", and neither of the usual answers gets anywhere near it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general form
&lt;/h2&gt;

&lt;p&gt;The control experiment is the whole technique, and it generalises past no-code builders:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;To separate the platform's cost from yours, hold the platform constant and vary only your content.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same account, same stack, same day. A near-empty page in the same system is the floor. Whatever your real page ships above that floor, you built — regardless of who wrote the code that emits it.&lt;/p&gt;

&lt;p&gt;This works for a CMS theme, a component library, an analytics stack, a framework's hydration payload. Every one of those arguments usually dead-ends in "well, that's just what X does". The empty-page control turns it into a number, and a number is something an owner can make a decision about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I measured and then threw away
&lt;/h2&gt;

&lt;p&gt;This matters more than the findings, because a wrong claim about someone's site costs you the entire conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The search box.&lt;/strong&gt; His actual complaint was that site search felt slow. I tried hitting the platform's search endpoint directly and got &lt;strong&gt;400 Bad Request&lt;/strong&gt; three times in a row. It was tempting to write "your search endpoint is returning 400" — it would have looked like a sharp catch. It would also have been a lie. Three consecutive 400s from an undocumented endpoint mean &lt;em&gt;I guessed the request format wrong&lt;/em&gt;, not that his search is broken. What went back to him instead was: I could not measure this from outside, and here is how you check it yourself in a minute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load time.&lt;/strong&gt; The document arrived in 4,754 / 4,896 / 5,106 / 5,114 ms. Beautifully consistent numbers — and useless as an argument, because that is &lt;em&gt;my&lt;/em&gt; connection to his server, not what his customer experiences on the other side of an ocean. Weight is a property of the page. Time is a property of the path between two machines. Only one of those belongs in a claim about someone else's site.&lt;/p&gt;

&lt;p&gt;There were also 21 platform apps installed. I mentioned it as a direction and gave &lt;strong&gt;no number&lt;/strong&gt;, because I didn't measure each one's contribution and I'm not going to invent it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother being this careful
&lt;/h2&gt;

&lt;p&gt;Because the whole value of an unsolicited measurement is that it's true. The moment one number in it is wrong, the recipient is right to discard all of them — and they will, along with the person who sent it.&lt;/p&gt;

&lt;p&gt;The discipline is cheap: reproduce cold three times, run a control that holds the platform constant, and delete anything you cannot separate from your own network, your own cache, or your own guess at an API. What survives is short, and it's worth something.&lt;/p&gt;




&lt;p&gt;I build browser tools where this kind of thing is the job — parametric product configurators and engineering calculators, where the geometry and the costing are computed from the same parameters so they can't disagree. Work at &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt;, reachable on Telegram at &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; or at &lt;a href="mailto:paladei702@gmail.com"&gt;paladei702@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run this control on your own stack, I'd be curious what your floor turns out to be. Mine keeps coming in higher than anyone expects.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>Four false positives in one evening: telling a broken web app from a broken measurement</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 21:51:14 +0000</pubDate>
      <link>https://dev.to/smirnovartur/four-false-positives-in-one-evening-telling-a-broken-web-app-from-a-broken-measurement-5gd3</link>
      <guid>https://dev.to/smirnovartur/four-false-positives-in-one-evening-telling-a-broken-web-app-from-a-broken-measurement-5gd3</guid>
      <description>&lt;p&gt;I spent an evening opening other companies' product configurators — 3D and parametric tools on manufacturers' sites — looking for things that were genuinely broken. Twenty-seven of them.&lt;/p&gt;

&lt;p&gt;The findings were real. But the part worth writing down is that &lt;strong&gt;four separate times in one evening, my tooling told me an application was broken when it was fine.&lt;/strong&gt; Every one of those four passed automated checks that looked rigorous. What caught them was a screenshot.&lt;/p&gt;

&lt;p&gt;If you write scripts that judge pages you don't own — uptime checks, competitor teardowns, scraping health, QA of an embedded widget — you will hit these. Here is the full list of signals that lied to me, and the one control that never has.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four false positives
&lt;/h2&gt;

&lt;p&gt;All four produced the same symptom: &lt;strong&gt;no &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; on the page, and an almost empty &lt;code&gt;innerText&lt;/code&gt;.&lt;/strong&gt; That looks damning when the page is literally titled "Configurator". It is also what three completely healthy situations look like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The tool starts on a click.&lt;/strong&gt; An orange button launches it. My script measured an unopened door and reported an empty room. Four automated passes — raw HTTP with a browser UA, my own browser, two runs from a clean profile, a control on the same domain — all four confidently examined a page that hadn't started yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The entire UI lives inside the canvas.&lt;/strong&gt; One hall configurator draws its menus, its undo/redo and its PDF export in WebGL. Empty DOM text is &lt;em&gt;correct&lt;/em&gt; there, not a defect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool is behind a login.&lt;/strong&gt; I was measuring a sign-in page. Fifty-four characters of text and one button reading "Anmelden".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The page is a landing page about the configurator,&lt;/strong&gt; not the configurator.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No network-level or DOM-level check distinguishes these from an actual failure. A screenshot distinguishes all four instantly.&lt;/p&gt;

&lt;p&gt;So the first rule I now follow, before any measurement at all:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Take the screenshot first. Look at the picture. What you cannot see in the image, you do not measure.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It costs one second and it is the highest-yield step in the whole process. The corollary is that &lt;strong&gt;automation answers "is it reachable", never "does it work."&lt;/strong&gt; When you catch yourself designing a fifth automated pass, what you actually need is one human opening the page and clicking the obvious thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signals that lied, and why
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"No canvas" and "empty innerText".&lt;/strong&gt; The two least reliable signals I use. Demoted permanently: they are now a reason to look at a screenshot, never a finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading pixels off someone else's WebGL canvas.&lt;/strong&gt; I nearly built a proof-of-emptiness on this. Copy the canvas into a 2D context, count non-transparent pixels, get zero — on a perfectly live scene. WebGL clears the drawing buffer after compositing unless &lt;code&gt;preserveDrawingBuffer&lt;/code&gt; is set. The zero is an artefact of the spec, not evidence about their app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ERR_ABORTED&lt;/code&gt;, even on the company's own domain.&lt;/strong&gt; One site showed eight failed requests, its own domain, its own scripts. That passed my "only count first-party failures" filter and looked like the real thing. Re-run waiting for &lt;code&gt;networkidle&lt;/code&gt; instead of &lt;code&gt;domcontentloaded&lt;/code&gt;: sixty-five responses, all 200, zero errors. &lt;strong&gt;My own measurement was cutting the page off mid-load.&lt;/strong&gt; A failure is a 4xx or 5xx &lt;em&gt;status in a response&lt;/em&gt; — not a request that got aborted, and you must wait for the network to settle before you believe anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A third-party widget failing.&lt;/strong&gt; Their chat widget threw &lt;code&gt;net::ERR_FAILED&lt;/code&gt; plus CORS. Real, reproducible — and it did exactly the same thing on two unrelated sites, in different countries, on different accounts. That is my environment being filtered, not their code. Same for analytics, tag managers and captcha endpoints: when those fail, you have measured your own ad blocker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;transferSize&lt;/code&gt; from a persistent browser profile.&lt;/strong&gt; Returns zero or garbage, because the response came from cache. Weight gets measured from a clean profile or not at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;403 with no User-Agent.&lt;/strong&gt; One door configurator returned 403 and 118 bytes to a headless request, and 200 to the same request with a real browser UA. The site was healthy; my headless client was being filtered, and I almost filed it as broken. &lt;strong&gt;Without a genuine User-Agent, every 403, timeout and empty body you collect is worthless.&lt;/strong&gt; Likewise 403/503 from a WAF: that is a block against a datacentre IP, not a defect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 300×150 canvas.&lt;/strong&gt; That is the HTML default and proves nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single beautiful observations that didn't reproduce.&lt;/strong&gt; I saw requests to a URL containing a literal &lt;code&gt;undefined/undefined&lt;/code&gt;. Gorgeous finding. It did not appear on either re-run, so it does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control that always works
&lt;/h2&gt;

&lt;p&gt;Everything above is one question wearing different costumes: &lt;strong&gt;am I being filtered?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a while I answered it with external text proxies — fetch the same URL through a service on another network, compare. Then all four services I used went down at once: 403, 522, 521, 429. Which taught me the more useful lesson — I ran a &lt;em&gt;known-good&lt;/em&gt; file through the same proxies and got the same 521 back. The proxies were down, not the sites. An error from your verification tool is not evidence about your target.&lt;/p&gt;

&lt;p&gt;The replacement is better than what it replaced, needs nothing external, and works every time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Fetch the neighbouring object, from your own network, in the same request.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;A 404 on a file? Request the file next to it in the same directory.&lt;/li&gt;
&lt;li&gt;An empty page? Request a different page on the same domain.&lt;/li&gt;
&lt;li&gt;A dead domain? Request their homepage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The logic is airtight. &lt;strong&gt;Geo-filters, datacentre blocks and User-Agent screening kill the whole connection.&lt;/strong&gt; None of them can selectively 404 one filename and serve its neighbour.&lt;/p&gt;

&lt;p&gt;A worked example. A decor texture returned 404 at 564 bytes. Its neighbour in the same directory, same User-Agent, same client, same second, returned 200 at 1877 bytes. No filtering mechanism in existence produces that. The file is genuinely missing. Another: a site whose configurator subdomain returned 200 with a 31-byte body — the entire payload being an HTML comment reading &lt;code&gt;&amp;lt;!-- PHP Translation array --&amp;gt;&lt;/code&gt; — while its main domain served me full pages normally.&lt;/p&gt;

&lt;p&gt;For JavaScript errors specifically, note that a text proxy is useless &lt;em&gt;in principle&lt;/em&gt;: it doesn't execute scripts, so it can only confirm HTML delivery. There, reproduction from a clean profile plus a control page on the same domain is what you have — or a human on a different connection, whose home ISP is more convincing than any proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What survives all of it
&lt;/h2&gt;

&lt;p&gt;These findings don't depend on clicks, environments or filters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;4xx/5xx status from the company's own domain&lt;/strong&gt;, with a neighbouring object returning 200.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;response body too small to contain anything&lt;/strong&gt; — 31 bytes has nothing to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No TCP connection at all.&lt;/strong&gt; My strongest confirmed case: &lt;code&gt;curl&lt;/code&gt; returns code 000 and zero bytes, my browser times out at 45 seconds, an external network reports &lt;code&gt;ERR_ADDRESS_UNREACHABLE&lt;/code&gt;, and a direct TCP connection to their IP on ports 80 and 443 refuses. DNS resolves; there is no machine behind it. Meanwhile their main site is healthy and links that tool from the menu three times.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;library version read out of their own served file.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Screenshot first.&lt;/strong&gt; If you can't see it, stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch the interactive thing.&lt;/strong&gt; Click the button. An unopened tool measures as a broken one.&lt;/li&gt;
&lt;li&gt;Real User-Agent on every request, always.&lt;/li&gt;
&lt;li&gt;Wait for &lt;code&gt;networkidle&lt;/code&gt;. Count response statuses, not aborted requests.&lt;/li&gt;
&lt;li&gt;Reproduce twice, from a clean profile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the neighbour control.&lt;/strong&gt; No control, no finding.&lt;/li&gt;
&lt;li&gt;First-party domain only. Their scripts, their responses.&lt;/li&gt;
&lt;li&gt;Anything that fails a step gets dropped without regret.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One operational note that cost me an evening: &lt;strong&gt;give every site its own tab.&lt;/strong&gt; A single timeout leaves a navigation hanging, and every subsequent &lt;code&gt;goto&lt;/code&gt; fails with "interrupted by another navigation". Eight sites I had recorded as dead turned out to be one stuck window.&lt;/p&gt;




&lt;p&gt;The reason this discipline is worth the trouble: I use these findings to start conversations with the companies that own the tools. A message that says "your configurator throws thirteen unhandled exceptions on every load, here they are, I checked twice and from a second network" is worth a great deal — and a message that says it about a tool which is &lt;em&gt;actually fine&lt;/em&gt; closes that door permanently and poisons every other message you send. The rigour isn't perfectionism. It's the thing that makes the finding usable at all.&lt;/p&gt;

&lt;p&gt;I build the kind of tool I was auditing — parametric product configurators and engineering calculators for the browser, where the geometry and the costing are computed from the same parameters so they can't drift apart. Work at &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt;, and I'm reachable on Telegram at &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt; or at &lt;a href="mailto:paladei702@gmail.com"&gt;paladei702@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you audit pages you don't own, I'd like to hear which signal fooled you — I suspect the list above is incomplete in ways I can't see from where I'm standing.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>testing</category>
      <category>webperf</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Zero bytes of geometry: a metal lattice sphere-traced in a 14.9 KB page</title>
      <dc:creator>Artur Smirnov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 21:04:59 +0000</pubDate>
      <link>https://dev.to/smirnovartur/zero-bytes-of-geometry-a-metal-lattice-sphere-traced-in-a-149-kb-page-kib</link>
      <guid>https://dev.to/smirnovartur/zero-bytes-of-geometry-a-metal-lattice-sphere-traced-in-a-149-kb-page-kib</guid>
      <description>&lt;p&gt;There is no model file on this page. No mesh, no texture, no &lt;code&gt;.glb&lt;/code&gt;, no HDR environment map. The object you orbit is a mathematical function evaluated per pixel, and the whole thing — renderer, distance field, post chain — arrives in a 14.9 KB HTML document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://smirnov-artur.github.io/webgl/lattice/" rel="noopener noreferrer"&gt;https://smirnov-artur.github.io/webgl/lattice/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the network panel before you judge that claim. Four requests, 64.5 KB over the wire, and &lt;strong&gt;49 KB of that is two web fonts&lt;/strong&gt; — the typography outweighs the renderer three to one. The counter in the corner of the page reads &lt;code&gt;GEOMETRY 0 B&lt;/code&gt; because it is telling the truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the object actually is
&lt;/h2&gt;

&lt;p&gt;A graded gyroid: a triply-periodic minimal surface, the structure used in additive manufacturing when you want stiffness or heat transfer without mass. The entire solid is five lines of signed distance field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yzx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// gyroid, TPMS&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;wall&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="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// shell around the surface&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// outer skin&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&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;30&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;                &lt;span class="c1"&gt;// inner cavity&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;       &lt;span class="c1"&gt;// section wedge subtracted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;k&lt;/code&gt; is a function of radius, not a constant. That is the "graded" part: the cell gets finer toward the core, the way a real printed lattice is graded where the loads concentrate.&lt;/p&gt;

&lt;p&gt;The section cut is the part people ask about. It is two half-spaces subtracted from the field — which is &lt;em&gt;why&lt;/em&gt; the cut face shows a genuine cross-section through the lattice instead of a hollow capped shell. Drag the SECTION slider and you are re-cutting a solid, not playing back an animation of a pre-made mesh. There is no mesh to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that took the longest
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;An analytic entry test.&lt;/strong&gt; Every ray gets a ray/bounding-sphere intersection first. Pixels that miss the specimen cost two dot products instead of a full march. On a shot where the object covers a third of the frame, that is most of your pixels bought for nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silhouette coverage out of failure.&lt;/strong&gt; Grazing rays run out of march steps before they converge — that is what makes ray-marched edges crunchy and full of holes. Instead of throwing those rays away, I keep their closest approach to the surface and reuse it as partial coverage. The rim fills in and the edge antialiases, and it costs nothing extra because the number was already computed and discarded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No HDR file.&lt;/strong&gt; The environment is a two-stop sky plus a warm softbox and a cool rim, each a broad cosine lobe with a tight core. Roughness widens the lobe instead of sampling a blurrier mip, so the whole image-based lighting is about a dozen instructions and zero bytes downloaded. This is the single biggest reason the page weighs what it does: an HDR environment is usually the heaviest asset in a scene like this, and here it does not exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metal BRDF&lt;/strong&gt; with roughness-aware Fresnel, per-channel dispersion (the reflected ray is fanned around the view tangent, wider at grazing angles), and heat-tempering colours — straw into blue — graded by depth into the core. SDF soft shadows and AO, skipped entirely on faces turned away from the key light. A volumetric core accumulated in front of the first hit, so the glow only leaks out through open cells and the section cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The post chain is hand-written too:&lt;/strong&gt; bright-pass with a soft knee, two levels of separable Gaussian, then a composite doing ACES (Hill fit), edge-weighted chromatic aberration, unsharp, vignette, grain and dither, converting to linear Display-P3 where the display supports it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The performance trick worth stealing
&lt;/h2&gt;

&lt;p&gt;Quality here is expressed as &lt;strong&gt;ray-march pixels per CSS pixel&lt;/strong&gt;, not as a fixed resolution. A 1× laptop should not be undersampled and a 3× phone should not be melted, and those are the same setting expressed correctly.&lt;/p&gt;

&lt;p&gt;The adaptive scaler measures frame time against an &lt;em&gt;estimate of the refresh period&lt;/em&gt; rather than against a fixed 16.7 ms. This matters more than it sounds. If you compare against 16.7 ms on a vsynced display, every frame looks marginally "slow", the scaler ratchets down, and it can never climb back — you end up at half resolution on hardware that had headroom the whole time.&lt;/p&gt;

&lt;p&gt;Context is requested with &lt;code&gt;alpha:false, antialias:false, depth:false, stencil:false, powerPreference:high-performance&lt;/code&gt;, and a first attempt at &lt;code&gt;failIfMajorPerformanceCaveat:true&lt;/code&gt;. A null answer there means the browser would fall back to a software rasteriser — so the low tier gets selected up front instead of after the user watches five seconds of slideshow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measured, not estimated
&lt;/h2&gt;

&lt;p&gt;Desktop, RTX 3050, hardware ANGLE/D3D11:&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;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FPS&lt;/td&gt;
&lt;td&gt;60 (vsync ceiling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page weight over the wire&lt;/td&gt;
&lt;td&gt;64.5 KB across 4 requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;of which, two web fonts&lt;/td&gt;
&lt;td&gt;49 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the document carrying the entire renderer&lt;/td&gt;
&lt;td&gt;14.9 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;geometry + textures&lt;/td&gt;
&lt;td&gt;0 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The march resolution sits around 0.9× CSS pixels on the laptop and about 0.5× device pixels on a 390×844 phone at DPR 3, adapting continuously.&lt;/p&gt;

&lt;p&gt;One honest correction, because I got this wrong in my own notes first: an earlier version said 95 KB. That was the &lt;em&gt;uncompressed&lt;/em&gt; total. Over the wire, gzipped, it is 64.5 KB. Both numbers were real measurements — they just measured different things, and I had not said which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build it this way
&lt;/h2&gt;

&lt;p&gt;Because "it's only 14.9 KB" is not the point — it is the receipt. A distance field is a &lt;em&gt;parametric&lt;/em&gt; object. The three sliders on that page are not presets, they are terms in the equation, and the solid re-forms around them because there was never a fixed model to violate. That property is the whole reason I use SDFs for engineering work: when the client says "now make the wall thinner", nothing has to be re-exported.&lt;/p&gt;

&lt;p&gt;Raw WebGL 2, no library, hand-written GLSL, one HTML file. Drag to orbit — the camera is on a critically damped spring rather than a linear follow, which is a small thing you feel immediately and never consciously notice.&lt;/p&gt;




&lt;p&gt;If you build things like this, I would genuinely like to hear how you handle grazing-ray coverage — the closest-approach trick is the best I have found, but I doubt it is the best there is.&lt;/p&gt;

&lt;p&gt;I take on WebGL work, mostly real-time product configurators and engineering visualisation. More at &lt;a href="https://smirnov-artur.github.io/webgl" rel="noopener noreferrer"&gt;smirnov-artur.github.io/webgl&lt;/a&gt;, or Telegram &lt;a href="https://t.me/smirnovarturr" rel="noopener noreferrer"&gt;@smirnovarturr&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>graphics</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>webgl</category>
    </item>
  </channel>
</rss>
