Tracking Page Weight Over Time
Performance budgets are thresholds: they fail a pull request when a page crosses a line. They are good at catching the one big mistake — a 200 KB library added to every page — and blind to the more common failure, which is slow growth. A tracking script here, a font weight there, a new component with its own CSS, a larger default image quality: each change is a few kilobytes, well inside the budget, and together they add up to a page that weighs 40% more than it did a year ago, discovered only when someone finally opens the Network panel.
This guide records page weight per template on every production deploy of a static site, stores the history cheaply, charts it on an internal page, attributes jumps to commits, and turns the trend into decisions about budgets. It is part of Performance Budgets and Lighthouse CI, and uses the template manifest from Per-Template Performance Budgets.
Prerequisites
- A build that can be inspected in CI, and a list of representative URLs per template.
- Git history available in CI (to record commit metadata).
- A place to store a small history file.
What Happened Without Tracking
Reconstructing weight from old builds (by checking out a commit per month and building it) showed how the docs article template had grown over eighteen months:
| Month | JS | CSS | Fonts | Images (typical page) | Total |
|---|---|---|---|---|---|
| 0 | 18 KB | 9 KB | 48 KB | 140 KB | 215 KB |
| 6 | 31 KB | 12 KB | 72 KB | 150 KB | 265 KB |
| 12 | 44 KB | 14 KB | 96 KB | 165 KB | 319 KB |
| 18 | 52 KB | 16 KB | 96 KB | 188 KB | 352 KB |
Each monthly step was under the 60 KB JavaScript budget and the 400 KB total budget. Over eighteen months, the page grew 64%, and field LCP p75 rose from 1.4 s to 1.8 s.
Step 1: Measure From the Build on Every Deploy
A script runs after each production build. For each representative URL, it parses the HTML, resolves resources referenced in the initial HTML (scripts, stylesheets, preloads, images above a size threshold, fonts referenced from the stylesheet), and sums compressed sizes from the build output:
// scripts/measure-weight.mjs (core)
const out = [];
for (const { layout, urls } of manifest) {
for (const url of urls) {
const resources = await resolveResources(`dist${url}index.html`); // [{ path, type, thirdParty }]
const sizes = await Promise.all(resources.map(async (r) => ({ ...r, bytes: r.thirdParty ? r.knownBytes : brotliSize(await readFile(`dist${r.path}`)) })));
const byType = sumBy(sizes, (s) => s.type);
out.push({ date: new Date().toISOString(), sha: process.env.GITHUB_SHA, layout, url, ...byType,
total: sizes.reduce((a, s) => a + s.bytes, 0), requests: sizes.length });
}
}
await appendFile('weight.jsonl', out.map((o) => JSON.stringify(o)).join('\n') + '\n');
Third-party resources are not in the build output; their sizes come from a small table maintained by hand and refreshed monthly with a real browser measurement, which is accurate enough for trends. The script takes about three seconds.
Step 2: Store the History
The history is a JSON Lines file on a dedicated perf-data branch. The deploy workflow checks out that branch in a separate directory, appends the new lines and pushes:
- uses: actions/checkout@v4
with: { ref: perf-data, path: perf-data }
- run: node scripts/measure-weight.mjs && cat weight.jsonl >> perf-data/weight.jsonl
- run: |
cd perf-data
git config user.name "perf-bot" && git config user.email "perf-bot@users.noreply.github.com"
git add weight.jsonl && git commit -m "weight: ${GITHUB_SHA::7}" && git push
After a year of about four deploys a day, the file was 2.1 MB — trivially small, diffable, and queryable with jq or DuckDB. Keeping it on its own branch avoids noise in the main history.
Step 3: Chart It
An internal page, itself a small static build, reads weight.jsonl and draws one line per template for total weight, with stacked areas by resource type on hover. Each point links to the commit. The same page shows the budget line for each template, so the gap between current weight and budget is visible — when a template sits far under budget for months, the budget is tightened, following the practice in Writing a Performance Budget That Fails Builds.
Step 4: Watch for Slow Growth
Budgets catch jumps; the weekly growth report catches drift. It compares the median weight per template over the last four weeks with the four weeks before and posts any template that grew more than 5% to the team channel, with the three commits that added the most bytes. In the first three months it flagged the blog template twice — once for a new analytics event library (+9 KB), once for a gradual rise in image weight traced to an editor uploading PNG screenshots that the pipeline converted at higher quality than JPEG sources.
Step 5: Tie Weight to Field Metrics
Weight is a proxy; field LCP and INP are the outcomes. Plotting the docs template's weight against its field LCP p75 by month showed the relationship clearly on this site — each 50 KB added about 0.1 s at p75 on mobile — which gave the team a concrete way to argue about additions: "this widget costs 30 KB, roughly 60 ms of LCP for our readers". The field side comes from the dashboard in Building a Core Web Vitals Dashboard from RUM Data.
Making the Trend Part of Planning
Data only helps if someone looks at it at the moment decisions are made. The team added two habits. At the start of each quarter's planning, the weight chart for the three biggest templates is on screen for five minutes, and any template that grew more than 10% over the quarter gets a small line item to recover it. And any proposal that adds a new component, library or embed to a high-traffic template includes its expected weight, checked afterwards against the recorded number. Neither habit takes long; together they meant the page weight trend on this site has pointed down for four consecutive quarters.
Measured Impact
| Measure | Before tracking | After one year |
|---|---|---|
| Docs article weight | 352 KB | 238 KB |
| Blog post weight | 410 KB | 296 KB |
| Field LCP p75, docs (mobile) | 1.8 s | 1.45 s |
| Budgets tightened based on trend | 0 | 5 |
| Slow-growth episodes flagged by the weekly report | — | 7 |
| Cost of the system | — | ~3 s per deploy, 2.1 MB of data |
Pitfalls & Rollback
- Measuring with a browser on every deploy. Slow and noisy for trends; measure bytes from the build.
- No commit metadata. Without the SHA, a jump in the chart cannot be explained.
- Tracking only totals. A per-type breakdown shows whether scripts, images or fonts grew.
- Ignoring third-party weight. It is outside the build; keep a maintained estimate and refresh it.
- Rollback: the measurement step and data branch are independent of the site; removing the step stops collection without affecting deploys.
Conclusion
Pages rarely get heavy in one step; they drift. Record weight per template from the build on every deploy, keep the history on a data branch, chart it with budget lines, and review a weekly growth report. On this site a year of tracking reversed eighteen months of drift, took the docs template from 352 KB back to 238 KB, and gave every "small" addition a visible cost.
FAQ
Why track page weight when budgets already exist?
Budgets catch a single change that crosses a line. Weight tracking catches slow growth, where many small additions each stay under the budget until the page is much heavier than it was a year ago. The trend also shows when a limit should be tightened because the site has become lighter.
What should be recorded?
Compressed transfer bytes per resource type for representative pages of each template, request counts, third-party bytes, and the commit and date. Recording from the build output makes the numbers deterministic and cheap.
Where should the data live?
Anywhere append-only and easy to query. A JSON Lines file committed to a separate branch, a small SQLite database stored as a CI artifact, or a time-series store all work. The site measured here used a JSON Lines file on a data branch and a static chart page built from it.
How do I attribute a jump to its cause?
Record the commit SHA with every data point. When the chart jumps, the commit range between the two points is short, and the per-type breakdown shows whether scripts, styles, images or fonts grew, which usually points straight at the change.
Related
- Parent: Performance Budgets and Lighthouse CI — budgets and CI.
- Per-Template Performance Budgets — the manifest this uses.
- Tracking Bundle Size per Pull Request — the pull request view.
- Measuring Build Time Regressions in CI — the same idea for build time.
- Tracking Deploy Markers in Analytics — linking deploys to field changes.
Filed underHow-tos