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:

MonthJSCSSFontsImages (typical page)Total
018 KB9 KB48 KB140 KB215 KB
631 KB12 KB72 KB150 KB265 KB
1244 KB14 KB96 KB165 KB319 KB
1852 KB16 KB96 KB188 KB352 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.

Growth of the docs article template weight over time A timeline of total compressed page weight for the docs article template. Month 0 at 215 kilobytes. Month 6 at 265 kilobytes after a second font weight and a feedback widget. Month 12 at 319 kilobytes after a search library upgrade and a third font family. Month 18 at 352 kilobytes after higher default image quality. No single change broke the budget. Docs article weight over eighteen months Month 0 Month 6 Month 12 Month 18 215 KB baseline 265 KB font weight, feedback widget 319 KB search upgrade, third font 352 KB image quality 75 to 85
Reconstructed by building one commit per month and measuring the same representative page.

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.

Pipeline for recording and charting page weight Four stages. On every production deploy, the build output is measured per template from the HTML and compressed file sizes. The result, with commit SHA and date, is appended to a JSON Lines file on a data branch. A small static dashboard reads the file and charts weight per template with budget lines. A weekly job compares the last four weeks with the four before and posts templates that grew more than 5 percent. How the history is built Measure on deploy per template, from dist ~3 s per deploy Append to weight.jsonl perf-data branch 2.1 MB after a year Static dashboard lines per template, budget lines links each point to its commit Weekly growth report 4 weeks vs previous 4 flags growth over 5%
The whole system is a script, a branch and a static page; there is no database to run.

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.

Docs article weight and field LCP before tracking and after one year Paired bars. Total weight of the docs article template went from 352 kilobytes at the start of tracking to 238 kilobytes a year later. Field LCP p75 on mobile went from 1.8 seconds to 1.45 seconds. Docs article template, before and after a year of tracking Start of tracking One year later Total weight 352 KB 238 KB Field LCP p75 (x10 ms scale) 1.8 s 1.45 s
Weight from the history file; LCP from RUM, 28-day windows.

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

MeasureBefore trackingAfter one year
Docs article weight352 KB238 KB
Blog post weight410 KB296 KB
Field LCP p75, docs (mobile)1.8 s1.45 s
Budgets tightened based on trend05
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.

Filed underHow-tos