Skip to main content
Article

Your Page Weight Numbers Are Probably Wrong: Decoded Bytes, Transfer Bytes and the Cache

6 min read 37 views

The first draft of a performance audit on a small website we maintain reported a homepage about three times heavier than the one visitors actually download. Bootstrap's CSS alone was listed at 190 kB, and the draft proposed a custom Bootstrap build and an icon-library migration to cut the total. When we reviewed it against the running site, the homepage came to a few hundred kilobytes on the wire and Bootstrap to 27 kB. The audit had measured the wrong thing, and every recommendation built on those figures had to be re-ranked.

This post covers where the wrong number came from, how to measure page weight properly from the browser, and what turned out to be worth fixing once the numbers were right.

Three sizes for every resource

The browser's Resource Timing API reports three sizes for every file a page loads:

  • transferSize: bytes that crossed the network, including headers. Reads 0 when the file came from cache.
  • encodedBodySize: the body as it arrived, still compressed.
  • decodedBodySize: the body after the browser has decompressed it.

The audit used decodedBodySize. On a server with gzip enabled, that figure is what the browser holds in memory, not what the visitor downloaded. For text assets (HTML, CSS, JavaScript, SVG) the difference is large: gzip typically shrinks them four to seven times.

AssetDecodedOn the wire
Bootstrap CSS190 kB27 kB
Icon font CSS58 kB13 kB

Images and woff2 fonts were reported correctly, because those formats are already compressed and gzip leaves them almost unchanged. That made the error harder to spot: some figures in the table were right.

A quick way to see all three side by side from the browser console:

performance.getEntriesByType('resource').map(r => ({
  file: r.name.split('/').pop(),
  wire: r.transferSize,
  compressed: r.encodedBodySize,
  decoded: r.decodedBodySize
}));

Two caveats. transferSize reads 0 for anything served from cache, so run it on a cold load. And cross-origin resources report 0 for all three unless the server sends a Timing-Allow-Origin header. As a cross-check we request each same-origin asset with Cache-Control: no-cache and read the Content-Length of the compressed response.

The cache changes the question

The second thing the draft left out was the cache policy. The site's fingerprinted CSS and JavaScript are served with Cache-Control: public, max-age=31536000 (one year) and images with max-age=2592000 (30 days).

That means almost every byte in the audit is a first-visit cost. A returning visitor downloads the HTML and nothing else. It does not make the first visit unimportant, since that is the visit that decides whether someone stays. But it changes the cost and benefit of every optimisation. A day of refactoring to save 150 kB that only a first-time visitor pays for is a different proposition from the same saving on every page view.

Check more than one page

The same draft stated that reCAPTCHA loaded on every page. It had been observed on one page load. A crawl of every route showed it was already conditional: present only on the routes that post a form, absent from all the others.

Fonts gave a similar lesson in the other direction. Webfont files never appear in an asset list built from the DOM, because they are referenced only from CSS. And a font declared in @font-face is not necessarily downloaded: two of the icon font files on this site were declared but never fetched, because no glyph on the page used them. document.fonts reports each face as loaded or unloaded, which settles it. Taking both into account, the real first visit later measured lower still.

What was worth fixing

With the corrected numbers, four items survived, ranked by bytes saved per hour of effort:

  • Re-export an oversized logo. A certification badge was served as a PNG of well over 100 kB, many times larger than the small box it filled. PNG does not benefit from gzip. Re-exported as WebP at twice the display size, it saves nearly all of that. Ten minutes.
  • Compress SVG. The two logos were served as image/svg+xml with no Content-Encoding, tens of kilobytes each, while CSS and JavaScript were gzipped. SVG is text and compresses well. On Apache this is one line, AddOutputFilterByType DEFLATE image/svg+xml, and the logos shrink to a fraction of their size. Five minutes.
  • Set the reCAPTCHA language. The script URL carried only the render parameter, so Google served the English build and the privacy notice rendered in English on a site not written in English. Adding the hl parameter with the site's language code fixes it. No bytes saved, one line.
  • Preload the hero image. The largest element on screen was a CSS background-image on the header, which the browser cannot discover until the stylesheet has been parsed. A preload tag lets it start downloading immediately:
<link rel="preload" as="image" href="/img/hero.webp" fetchpriority="high">

That is well over 100 kB saved in under half an hour. The custom Bootstrap build was dropped: there is very little left to win from 27 kB. The icon migration was deferred, for reasons we cover in a separate post.

The same work on our own projects

We went through the same exercise on pinuno.com.gh in May 2026, for a different reason: many of our visitors in Ghana browse on small mobile data bundles, and every kilobyte comes out of their bundle. We enabled gzip (mod_deflate) and long-term browser caching (mod_expires and Cache-Control). We removed a Font Awesome 6 stylesheet linked from a CDN, although no view used a single Font Awesome class. And we replaced Google Fonts (Inter and JetBrains Mono) with the system font stack, which removed two network round trips.

Long caching then caught us out in a different way. In August we found eight CSS and JavaScript references served as immutable with a one-year max-age and no cache-busting query, so a returning visitor kept a stale copy for a year whatever we deployed. In July a change to scripts injected into our proxied course pages had silently failed to reach returning users for exactly this reason. The fix was one helper that appends ?v= and the file's modification time to every such reference. Long caching is only safe with a buster: without one, the cache that saves your visitors data also keeps your fixes away from them.

Our language-learning app met the same trap with audio rather than code. Its pronunciation MP3s are served as immutable, and in June 2026 we regenerated 659 of them in place, under the same filenames. Every browser that had already played one of those words would have kept the old recording for a year. In August the audio URLs gained a ?v= version parameter, bumped so every client fetches each file once more, and bumped again whenever existing files are regenerated under the same name.

Conclusion

Before ranking any performance work, measure what actually crosses the network, on a cold load, across every route, with the cache policy in view. The draft's figures were three times too high and its recommendations ranked accordingly. The corrected figures produced a shorter list that took half an hour to ship.

If you want to learn to build and measure PHP web applications like this one, have a look at our PHP Application Developer course.

K

Kwame Asante

Related Articles

Article Info

Type
Article
Category
Web Development
Published
14 Sep 2026
Reading time
6 min

Search

Put this into practice

Our structured courses take these topics from article to hands-on skill, taught by the practitioners who write them.

Browse Courses