Page weight, compression, and caching headers
How to send pages compressed with Brotli or gzip, set caching headers, and find the images, scripts, and other sites that make a page heavy.
Before a page is fully shown, the browser downloads the HTML and the files it links to: stylesheets, scripts, fonts, and images. Compression shrinks the text files on the way. Caching headers decide whether the browser, or a CDN, can reuse a file instead of downloading it again. And the total size decides how long all of it takes on a slow phone connection.
What SignalDNS checks
- Compression. We ask for the page a second time, without following redirects, saying we accept Brotli, gzip, and Zstandard (zstd), as browsers do. We read only the
Content-Encodingheader of the answer. - Caching. We show the page’s
Cache-Control,ETag,Last-Modified,Expires, andAgeheaders as the server sent them, without a judgment. - Page weight. We add up the HTML and the files it links to, and flag a heavy page, large images, uncompressed scripts and stylesheets, and files from other sites. The next section explains how.
Sizes use binary units: 1 KB is 1,024 bytes, and 1 MB is 1,024 KB.
How we weigh a page
We don’t run the page in a browser, so the total is an estimate:
- From the HTML, before any scripts run, we list the scripts (
<script src>), stylesheets, fonts (preloaded, or linked as.woff2,.woff,.ttf, or.otf), and images (<img src>). Anything inside<noscript>or<template>is left out. - We ask for each file with a HEAD request, which returns only headers, accepting compression as a browser does. Its
Content-Lengthis the size as sent, compressed or not. We weigh up to 40 files, within ten seconds. - Servers that compress on the fly often send no
Content-Length. We download those files once and count the bytes as sent, up to 2 MiB each. - The total is the HTML, uncompressed, plus those sizes.
What the total leaves out: anything scripts load after the page starts, images in srcset and CSS backgrounds, fonts named only inside stylesheets, files past the first 40, and files whose size we couldn’t get. When some sizes are missing, we say the total is a minimum. It also counts images with loading="lazy", which a browser may only fetch when someone scrolls to them.
The page is sent uncompressed
We asked for Brotli, gzip, or zstd, and the server sent the HTML without any of them. HTML is text and shrinks well; nginx’s documentation says gzip often halves the size or better. Browsers ask for it on every request. SignalDNS marks this as needing attention when the HTML is over 1 KiB.
When the page is compressed, we show which method was used. A page of 1 KiB or less sent uncompressed is only noted, since there’s little to save. If the second request didn’t get a 200 answer, such as a redirect, we show “Couldn’t ask”.
How to fix it: turn on compression for HTML, CSS, JavaScript, JSON, and SVG at your web server or CDN. Images and fonts in formats like JPEG, PNG, WebP, and WOFF2 are already compressed.
In nginx, gzip is built in and off by default. text/html is always included once it’s on:
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_vary on;
By default nginx doesn’t compress a request that came through a proxy, which it detects from a Via header; the gzip_proxied directive changes that. Brotli needs the separate ngx_brotli module.
In Apache, mod_deflate does gzip, and mod_brotli (2.4.26 and later) does Brotli the same way with BROTLI_COMPRESS:
AddOutputFilterByType DEFLATE text/html text/plain text/css text/javascript application/javascript
Cloudflare compresses text types such as HTML, CSS, and JavaScript by default. If a page through Cloudflare comes out uncompressed, check your Compression Rules, and whether your server sends Cache-Control: no-transform, which tells Cloudflare not to change the encoding.
Caching headers
These headers tell browsers and CDNs whether they may keep a copy of the page, and for how long. We show them as sent, with no judgment, because pages are often not cached on purpose: HTML changes, and visitors should see the latest version. Cloudflare, for example, doesn’t cache HTML by default.
Cache-Controlis the main one.max-age=600allows reuse for 600 seconds.no-cacheallows a copy but says to check with the server before each use.no-storesays not to keep one at all.privatekeeps it out of shared caches such as CDNs, for pages that differ per visitor.ETagandLast-Modifiedlet a browser ask “has this changed?” and get a short304 Not Modifiedanswer instead of the whole page. MDN suggests sending both.Expiresis the older way to set a lifetime, as a date. Whenmax-ageis also sent, it wins.Ageis how many seconds a copy has been sitting in a shared cache, such as a CDN. A value above 0 usually means the page came from that cache, not your server.
With no Cache-Control, browsers may still reuse a page for a while based on its Last-Modified date. MDN calls this heuristic caching and recommends always sending Cache-Control to avoid it.
A common setup, following MDN: Cache-Control: no-cache with an ETag for HTML, and a long max-age with immutable for scripts, stylesheets, and images whose file names change with each version, such as app.3f9a1c.js.
Cache-Control: no-cache
Cache-Control: max-age=31536000, immutable
The page is heavy
The page and the files it links to come to more than 4 MB. It all has to arrive before the page is complete, which is slowest on a phone connection, and it uses visitors’ data. SignalDNS marks this as needing attention.
For comparison, Lighthouse, Chrome’s audit tool, flags pages whose requests add up to more than 5,000 KiB. It loads the page in Chrome and counts everything, including what scripts fetch, so its total can differ from ours.
How to fix it: start with the five largest files, which Details lists by size. For images, see the next section. Then remove scripts the page doesn’t need, and make sure scripts and stylesheets are compressed.
Images over 1 MB
At least one image is over 1 MB as sent. We name up to three. An image that size may be larger than the space it fills on screen, or saved in a format that compresses poorly. SignalDNS marks this as needing attention.
How to fix it:
- Resize it to the largest size it’s shown at, allowing for high-density screens.
- Use a modern format. MDN describes WebP and AVIF as compressing much better than JPEG or PNG.
- Offer several sizes with
srcsetandsizes, so phones download a smaller file. - Lazy-load images further down the page with
loading="lazy". web.dev advises against it for images visible when the page loads, especially the largest.
<img src="/images/loaf-800.webp"
srcset="/images/loaf-400.webp 400w, /images/loaf-800.webp 800w, /images/loaf-1600.webp 1600w"
sizes="(max-width: 600px) 100vw, 600px"
width="800" height="600" loading="lazy"
alt="Sourdough loaf on a board">
We weigh the file in src. A phone choosing from srcset may download a different one.
Scripts or stylesheets are sent uncompressed
A script or stylesheet over 10 KB came back without a Content-Encoding, even though we asked for Brotli, gzip, or zstd. We name up to three. SignalDNS marks this as needing attention.
How to fix it: turn on compression for text/css and application/javascript at the server or CDN that serves them, as in the compression section. In nginx, these types aren’t compressed until you add them to gzip_types. If the files come from another site, check whether it offers a compressed version, or host them yourself.
Files from other sites
This is a note, not a judgment: how many weighed files come from other sites, and which hostnames. A file counts as another site’s when its registrable domain differs, so cdn.example.com is part of example.com.
Each other site can need its own DNS lookup and secure connection before its file loads, and the page depends on that site being up and fast.
What you can do: remove scripts you no longer use. Load the rest with async or defer so they don’t hold up the page. For a site the page always needs early, add <link rel="preconnect" href="https://cdn.example.com">. If a third-party server is slow, consider hosting the file yourself.
Setting it up in your CMS or framework
- nginx: compression and decompression, and the gzip module reference
- ngx_brotli, the Brotli module for nginx
- Apache: mod_deflate and mod_brotli
- Cloudflare: content compression and default cache behavior
- Next.js: compress (gzip with
next start), headers, and the Image component, which serves WebP withsrcset - Vercel: compression, with gzip and Brotli
- WordPress: responsive images, which adds
srcsetandsizesto images it generates
What we can’t tell from outside
We measure from one server without running scripts, so we can’t tell how fast the page actually loads. Sizes come from what each server reports. We check compression only for the HTML and the scripts and stylesheets it links to. We show the page’s caching headers but not those of each file, and we can’t see whether a visitor’s browser already has a copy.
Sources
- MDN: Content-Encoding
- MDN: HTTP caching, for heuristic caching,
Expiresandmax-age, and common patterns - MDN: Cache-Control and Age
- RFC 9111: HTTP Caching
- Lighthouse: avoid enormous network payloads
- MDN: using responsive images in HTML
- MDN: image file type and format guide
- web.dev: browser-level image lazy loading
- web.dev: third-party JavaScript performance and efficiently load third-party JavaScript
Last reviewed .