Lesson 9 of 9 · 22m
Guardrails: Keeping It Fast After You Fix It
Performance decays by default. Lab budgets in CI, field-data regression alerts, third-party governance, and the team habits that keep the gains from evaporating.
Every performance project I've seen succeed has the same epilogue: six months later, half the gains are gone. Nobody made a bad decision. Marketing added a heatmap tool, a new carousel shipped with lazy-loaded slides, someone imported a full utility library for one function. Performance is a property that decays by default — the fix isn't another project, it's guardrails that make regressions loud at the moment they're introduced.
Layer one: lab assertions on every PR
Lighthouse CI runs a production-like build against key routes and fails the check when metrics or resource sizes cross a line. Lab numbers are noisy, so assert on medians of several runs and leave margin.
{
"ci": {
"collect": {
"startServerCommand": "npm run preview -- --port 4173",
"url": [
"http://localhost:4173/",
"http://localhost:4173/product/trail-runner-3",
"http://localhost:4173/search?q=jacket"
],
"numberOfRuns": 5
},
"assert": {
"assertions": {
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["warn", { "maxNumericValue": 300 }],
"resource-summary:script:size": ["error", { "maxNumericValue": 180000 }],
"resource-summary:third-party:count": ["warn", { "maxNumericValue": 8 }]
}
},
"upload": { "target": "temporary-public-storage" }
}
}npx @lhci/cli@0.13.x autorun --config=./lighthouserc.jsonPair it with the size-limit gate from lesson 4: size checks are deterministic and fast, so they catch bundle growth reliably; Lighthouse catches the things size can't — a lazy hero, a render-blocking stylesheet, a layout shift.
Layer two: field alerts per release
Lab CI can't see real devices, A/B variants, or third-party tags injected at runtime. Your RUM pipeline (lesson 2) should compute p75 LCP, INP, and CLS per route and device class daily, and alert on a sustained regression — not a single noisy day.
-- p75 LCP on mobile per route, per release, last 7 days
SELECT route, release,
percentile_cont(0.75) WITHIN GROUP (ORDER BY value) AS p75_lcp,
count(*) AS samples
FROM rum_metrics
WHERE name = 'LCP' AND device = 'mobile' AND ts > now() - interval '7 days'
GROUP BY route, release
HAVING count(*) > 500
ORDER BY route, release;Because every beacon carries the release SHA, a regression points straight at a deploy window. Require a minimum sample size before alerting, or you'll page someone over twelve sessions from a bot.
Layer three: govern third parties
Third-party scripts bypass your bundler, your code review, and your CI. Treat them like dependencies: keep an inventory with an owner and a business justification for each, load them async or after interaction, review them quarterly, and measure their main-thread cost with the LoAF observer from lesson 8. A tag manager is a deployment pipeline that marketing controls; give it a budget too.
Make performance visible
- Put p75 field vitals for the top routes on the same dashboard as error rate and conversion.
- Add a "performance impact" line to the PR template for anything touching the initial route.
- When a budget is hit, the conversation is "what do we remove to make room," not "raise the limit."
Field notes
- Never gate PRs on the Lighthouse score. It's a weighted blend that changes between Lighthouse versions; assert on the specific metrics and sizes you care about.
- Run lab CI against a production build with production compression and caching headers — a dev server lies about everything.
- Expect CrUX to lag your fixes by up to 28 days. Use your own RUM for confirmation and CrUX for the external scorecard.
- The best guardrail is cultural: the team that sees the field numbers every week keeps them green.