---
title: "Nothing fails when a page gets heavier"
description: "Generated markup is verbose, imports whole libraries for one function, and inlines assets because that works. Every test still passes. The cost lands on a phone on a slow connection, months later, as a number with no cause attached."
url: https://harpywatch.com/blog/nothing-fails-when-a-page-gets-heavier
kind: article
author: "The HarpyWatch team"
published: 2026-10-13T09:00:00+00:00
tags: ["AI Agents", "Monitoring", "SEO"]
publisher: "HarpyWatch"
---

# Nothing fails when a page gets heavier

There is no red test for a page that got 40KB heavier. There is no red test for
the twelfth nested `div`, for `import _ from "lodash"` where `Array.prototype.at`
would do, or for a hero image pasted into the HTML as a base64 data URI because
that made it work without touching the asset pipeline.

All of those are the output of a competent, fast writer optimising for the
thing it was asked to optimise for: producing something that works. And each
one is invisible individually. That is the whole problem. Page weight has no
failure event. It has a slope.

## What generated markup actually does

Three habits, all of them locally reasonable.

**Verbosity.** Generated markup tends towards wrapper elements and long utility
class strings, because that is what most published markup looks like and
because nesting is the safe way to get a layout right without understanding the
surrounding CSS. Compression handles the transfer side of this well — repeated
class strings are exactly what gzip is good at — which is why it is easy to
dismiss. The part compression does not touch is the DOM: more nodes to build, more to style, more to
lay out, and every subsequent recalculation is over a larger tree. A page with
six thousand nodes and a page with fifteen hundred behave differently on a
mid-range Android phone in a way they do not on a developer's laptop.

**Whole-library imports.** An agent asked to format a date reaches for the
library it has seen used to format dates. Modern bundlers tree-shake, so
sometimes this is free — and sometimes it is not, because the library has side
effects, or a CommonJS build, or is pulled in by a path the bundler cannot
statically analyse. The failure is not that it always costs; it is that it
*sometimes* costs and nothing distinguishes the two cases at review time. A
diff that adds one import line looks the same either way.

**Inlining.** Inlining an asset removes a request, which is a real optimisation
in some situations, and there is a lot of published advice saying so. But base64
is about a third larger than the bytes it encodes, an inlined asset cannot be
cached separately from the document, and it is re-downloaded on every page
view, and it sits in the critical path of the HTML rather than being fetched in
parallel. A 200KB inlined hero image is a document that cannot start rendering
until 270KB has arrived.

None of this is incompetence. It is what the instruction "make this work"
produces, at a rate no review process was sized for.

## The mechanism, stated carefully

The path from bytes to money is real but it is longer than most posts about it
admit, so here it is with the joints exposed.

**Bytes and main-thread work become Core Web Vitals.** Largest Contentful
Paint is when the main content element renders, and it moves directly with how
long the critical resources take to arrive and parse. Interaction to Next Paint
measures how long the page takes to respond to a tap, and it degrades when the
main thread is busy — parsing and executing script is the usual culprit, and a
large DOM makes every style and layout pass afterwards more expensive.
Cumulative Layout Shift is only weakly about weight, but late-arriving fonts
and un-sized images are a common way generated markup earns it.

**Core Web Vitals are a ranking signal, and a small one.** This is the part to
be precise about, because it is routinely oversold. Google has said page
experience is used in ranking, and has also said it does not override having
the most relevant answer. A slow page about exactly what someone searched for
will outrank a fast page about something else. If your page is already the best
answer, shaving 300ms will not move you from position eight to position one,
and anyone who tells you otherwise is selling something. Where it plausibly
matters is between pages that are otherwise comparable, which is a real
situation and a narrower one than the usual pitch implies.

**Weight costs you visitors directly, and that link does not route through a
search engine at all.** Someone on a phone, on a bad connection, looking at a
blank screen, leaves. You do not have to take that on faith or on a vendor's
statistic: it is measurable in your own analytics, by load time bucket, against
your own funnel. That is the number worth pulling before you decide how much
this is worth to you.

So: the ranking effect is real and modest, and the abandonment effect is the
one you can measure yourself.

## Two settings that mitigate a surprising fraction of it

Before rewriting any markup, check the two settings that cost nothing.

**Compression.** Brotli or gzip on text responses — HTML, CSS, JavaScript, SVG,
JSON — is a large multiple on the transfer size of exactly the verbose output
described above, and it is a header and a config directive. It is also
routinely missing on one path and present on another: enabled at the CDN,
absent on the origin that serves the API; on for `.js` but not for the
`.mjs` a newer build emits; on for the HTML but not for the JSON payload the
page fetches immediately afterwards. The gap is rarely where you looked.

**Cache policy.** The second visit should not pay for the assets again. A
fingerprinted asset — one with a content hash in the filename — can be served
`Cache-Control: public, max-age=31536000, immutable` safely, and the HTML
document should be revalidated rather than cached hard. Getting this backwards
is the classic error, and it produces the two worst outcomes available: users
holding a stale page for a year, or re-downloading everything on every
navigation. An inlined asset, incidentally, cannot participate in any of this.

`compression` and `cache-policy` are both HarpyWatch checks — ours, so discount
accordingly — for the ordinary reason that both are properties of the response
a client receives rather than of the config you wrote. `page-weight` records
what actually arrived, so the slope is a series rather than a memory, and
`core-web-vitals` measures the rendering outcome those bytes produce.

The limit worth stating plainly: a scheduled check from a fixed location is a
lab measurement. It is excellent at telling you that something changed and
roughly when — which is the question you cannot otherwise answer — and it is
not a substitute for field data from your actual users on their actual devices.
Two different instruments. Use both if you have both.

## The point is the derivative

You do not need an alert on page weight. You need to know the number is going
up, and roughly when it started, because the answer to "why is the site slow
now" is almost always a commit, and finding it is trivial if you know the week
and impossible if you do not.

Nothing fails when a page gets heavier, so unless something is watching the
number, the first signal is a quarterly conversion report with no explanation
attached. Bisecting four hundred commits to find the cause is not impossible. It is just
expensive work to recover a fact that was free to record at the time.
