Your store weighs 1.7 MB and still takes 18 seconds: the number that matters is 364 requests
Page weight is the number everyone checks, and it is the reason this store has been slow for months without anyone finding out why. I measured a Shopify storefront twice, cold cache, an hour apart: | visit 1 | visit 2 | | |---|---|---| DOMContentLoaded | 1.2 s | 2.6 s | load | 18.7 s | 18.1 s | | transferred | 2 637 KB | 1 736 KB | | requests | 364 | 343 | 1.7 MB. By weight this is a healthy page - lighter than most of the stores I look at, and lighter than plenty of pages that finish in under two seconds. It still spends eighteen seconds finishing. Why the owner does not see it Look at the first row. The document is parsed and interactive in one to three seconds. The owner opens the shop, the header is there, the products are there, they scroll, they click, everything responds. Nothing about that experience says "eighteen seconds". The tail is not nothing, though. Until load fires you are still holding connections open, still running the browser's networking machinery, still competing for bandwidth on a phone that has very little of it. Images below the fold arrive late. Anything that hangs off window.onload - and on a store with a stack of apps, quite a lot does - is on the far side of that eighteen seconds. The third-party pixels the owner is paying for fire when the visitor has already left. And a visitor on a slow connection is not living in the owner's DOMContentLoaded . Three hundred sequential round trips on high-latency mobile is not "a bit slower than desktop", it is a different page. Why every weight-based check gives it a pass Because it is not heavy. That is the whole trap. The advice that comes back from a weight audit - compress your images, ship smaller JavaScript, lazy-load the hero - has nothing to bite on here. The scripts total 684 KB, which is unremarkable. There is no 6 MB PNG. There is no 4 MB logo. Every individual thing on the page is defensible, and there are three hundred and sixty-four of them. Compare with a store I measured the same week, where the weight was the story: a product page at 8 293 KB across 286 requests, of which 7 011 KB was twenty-seven theme images - and the identical 7 MB showed up on the home page too, because they were photographs inside a dropdown menu that shipped on every page in the shop. That one is a weight problem, and a weight tool finds it in thirty seconds. The 364-request store is the other class. Same symptom for the customer, opposite diagnosis, and one of the two diagnoses is invisible to the tool most people reach for. Measure it yourself, in the console, in ten seconds Open the site in a fresh incognito window with the network panel on and "Disable cache" ticked. Then paste this: const nav = performance.getEntriesByType('navigation')[0]; const res = performance.getEntriesByType('resource'); console.log('requests ', res.length); console.log('transferred ', (res.reduce((s, e) => s + e.transferSize, 0) / 1048576).toFixed(2), 'MB'); console.log('DOMContentLoaded', (nav.domContentLoadedEventEnd / 1000).toFixed(1), 's'); console.log('load ', (nav.loadEventEnd / 1000).toFixed(1), 's'); Two numbers matter and most reports only show you one. If load is many times domContentLoadedEventEnd , and the transferred total is unremarkable, you are looking at a request count problem and no amount of image compression will move it. To see where the requests go: const by = {}; for (const e of performance.getEntriesByType('resource')) { const k = e.initiatorType || 'other'; by[k] = by[k] || { n: 0, bytes: 0 }; by[k].n++; by[k].bytes += e.transferSize; } console.table(by); Sort by n , not by bytes . The row you are looking for is usually a long list of small files from a handful of hosts - an app that ships eleven scripts instead of one, an icon set delivered as individual SVGs, a font family loaded weight by weight, a review widget that fetches every avatar separately. Then the second half, which is the actual fix and which nobody can do for you from the outside: group the third-party hosts. const hosts = {}; for (const e of performance.getEntriesByType('resource')) { const h = new URL(e.name).host; hosts[h] = (hosts[h] || 0) + 1; } console.table(Object.entries(hosts).sort((a, b) => b[1] - a[1]).slice(0, 15)); Every distinct host is a fresh DNS lookup, TCP handshake and TLS negotiation before a single byte of content moves. On a desktop with a warm connection that is noise. On a phone on a cell network it is the majority of your eighteen seconds. If one host in that table is serving you forty files for a feature you do not use, you have found your afternoon's work. The check that stops you shipping a wrong number Two of these, both bought the hard way. Sum the parts and compare to the whole. I once produced a breakdown where CSS came out at 1 061 567 bytes on a page that transferred 1 323 722 bytes in total - CSS was eighty per cent of a page full of photographs, which is absurd on its face. The cause: images were being pulled in with , and my grouping used initiatorType , which reports those as link . The same PNGs were counted once as images and once as stylesheets. If your categories add up to more than the page, the measurement is wrong in full, not slightly off - go find the item you counted twice. Measure twice or say nothing. A single timing run is worth close to nothing. From the same batch 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 with load at 629 ms over 409 KB. A third gave 9.6 s, then 0.0 s. Three findings that would have been three confident, wrong emails. The store at the top of this article is in the article precisely because it survived: 18.7 s and 18.1 s, 364 and 343 requests, an hour apart. Numbers that repeat are findings. Numbers that appear once are hypotheses, and the coin lands heads about half the time. There is a third failure mode worth naming, because it looks like a finding and it is not yours: I measured the same home page twice and got 10 510 KB / 453 requests and then 4 162 KB / 492 requests. Two visits, one page, six megabytes apart. The difference was an embedded social feed pulling roughly 2.9 MB of images from someone else's CDN, and what it served varied per visit. The shop's own pages were light. If you are reporting a number to a client, know whose bytes you are counting. What I would actually do with this store In order, and only in this order: - Count requests per host and per feature. Not per file type - per feature. "The reviews app is 64 requests" is an actionable sentence. "Images are 210 requests" is not. - Delete before optimising. 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. - Bundle what is left. Icons into a sprite or inline SVG, fonts to the two weights you actually render, a script that ships eleven files into one. - Move what survives off the critical path so it is not holding load open -defer , or load on interaction. - Re-measure cold, twice, and compare load to the two runs above. If it did not move in both runs, it did not move. Eighteen seconds does not become one second. It becomes three or four, and the pixels the owner is paying for start firing while the customer is still on the page. If your own numbers look like the table at the top - small page, long tail - the two console snippets above will tell you which host is doing it, and that is genuinely most of the work. I do this kind of measurement and the repair that follows, mostly for storefronts and product sites. Portfolio: smirnov-artur.github.io/webgl ยท Telegram @smirnovarturr ยท pa********@gmail.com . Top comments (0)
Comments
No comments yet. Start the discussion.