<?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 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>
