Segmenting Baselines by Page Type
A performance baseline is only honest when it compares like with like. This guide, part of the Threshold Calibration & Baseline Management reference, shows how to split one site-wide baseline into a set of per-template, per-audience baselines so that a structurally heavy page never disappears into a pooled average dominated by lighter traffic. The core failure it prevents is silent: a checkout flow that ships 480 KB of third-party payment scripts can regress badly while your dashboards stay green, because a fast marketing home page carries the mean.
If you already track a single number for the whole property, treat this as the refactor that turns that number into a matrix. Everything downstream — the CI gate, the promotion job, the regression alert — becomes more sensitive the moment it stops averaging across page types that were never comparable to begin with.
Why One Global Baseline Lies
Consider a typical e-commerce property with four templates: a home page, a product listing (PLP), a product detail page (PDP), and a checkout flow. Their Largest Contentful Paint distributions are not variations of one shape — they are four different shapes, driven by different DOM sizes, hero media, hydration costs, and third-party load. On a mid-range mobile device throttled to Fast 3G, a realistic set of P75 LCP values might be 2100 ms for the home page, 3200 ms for the listing, 3800 ms for the detail page, and 4600 ms for checkout.
Now pool them. If the home page and listing together account for most sessions, the traffic-weighted P75 lands near 2900 ms — comfortably under a 3500 ms budget line. The pooled figure is arithmetically correct and operationally useless: it certifies the site as healthy while checkout, the single template where a slow load directly costs revenue, sits 1100 ms over budget. Averaging is a lossy compression that throws away exactly the signal you built the budget to catch.
The same distortion appears in the other direction. A single global baseline set tight enough to protect checkout will constantly flag the home page for "regressions" that are just normal variance on a page that was never near the limit. You end up with alert fatigue on one template and blind spots on another, from the same number. Percentile discipline, covered in Percentile-Based Threshold Tuning, fixes how you summarise a single distribution; segmentation fixes the prior question of which distributions you should never have merged.
Segmentation Axes: Template, Auth State, and Device
A segment is the intersection of the axes that materially change a page's performance profile. Three axes cover the vast majority of real sites, and you should adopt them in order of impact.
The first axis is template — the render path a route uses. Two URLs that share a template (/products/shoes and /products/bags) belong to one segment; two URLs on different templates never do. On a CMS this maps directly to the layout or content type, which is why Per-Template Baselines for CMS Sites treats the template registry as the source of truth for segment keys.
The second axis is authentication state. A logged-in account dashboard renders personalised data, skips the marketing chrome, and often ships behind a different bundle than its anonymous equivalent. Its LCP and INP distributions diverge enough that pooling them re-creates the original lie at a smaller scale. Separate Baselines for Logged-In and Anonymous Users goes deep on measuring the authenticated path without leaking credentials into CI logs.
The third axis is device class plus connection, because the same template on a mid-range Android over Fast 3G and on a desktop over cable are two distributions with no meaningful overlap. This axis is important enough that it has its own reference in Mobile vs Desktop Budget Divergence; here it is simply the third dimension of the segment key. A practical segment key is the tuple {template, auth, device} — for example pdp/anon/mobile.
Resist the urge to add more axes than you can populate with data. Every axis you add multiplies the number of segments and divides the samples per segment. Four templates times two auth states times two device classes is already sixteen segments; adding a locale axis with five values pushes you to eighty, most of which will be starved of samples. Start with template and device, add auth state only where the authenticated path is genuinely distinct, and stop.
A Baseline Schema Keyed by Segment
Store baselines in a single versioned file keyed by the segment tuple. A flat, greppable structure keeps the CI job simple and makes diffs readable in code review. Each entry records the metric values, the sample size and window that produced them, and the source URL that represents the segment.
{
"schemaVersion": 2,
"generatedAt": "2026-07-24T00:00:00Z",
"window": "trailing-14d",
"device": "moto-g-power",
"connection": "fast-3g",
"segments": {
"home/anon/mobile": {
"sampleUrl": "https://shop.example.com/",
"samples": 5400,
"lcpP75": 2100,
"inpP75": 180,
"clsP75": 0.04,
"tbtP75": 240
},
"plp/anon/mobile": {
"sampleUrl": "https://shop.example.com/c/shoes",
"samples": 3120,
"lcpP75": 3200,
"inpP75": 210,
"clsP75": 0.07,
"tbtP75": 360
},
"pdp/anon/mobile": {
"sampleUrl": "https://shop.example.com/p/runner-01",
"samples": 2740,
"lcpP75": 3800,
"inpP75": 240,
"clsP75": 0.09,
"tbtP75": 420
},
"checkout/auth/mobile": {
"sampleUrl": "https://shop.example.com/checkout",
"samples": 610,
"lcpP75": 4600,
"inpP75": 320,
"clsP75": 0.06,
"tbtP75": 680
}
}
}
The sampleUrl is deliberately singular. You do not need Lighthouse to crawl every product to protect the PDP template — one representative URL per template captures the render path, and real-user monitoring covers the rest of the population. That single-URL choice is what makes the CI matrix in the next section cheap enough to run on every pull request. The samples and window fields are what let you trust each number; the promotion mechanics that keep these values fresh are the subject of Historical Baseline Calibration, which this schema is designed to feed.
Deriving Per-Segment Thresholds
A baseline is an observation; a threshold is a decision. The threshold is what the gate compares against, and it should sit a defensible margin above the current per-segment P75 so that normal variance does not trip it while genuine regressions do. A simple, robust rule is: threshold equals the greater of the segment P75 plus an absolute floor, or the segment P75 times a relative multiplier — whichever is larger protects both fast and slow segments proportionately.
// Derive a gate threshold from a per-segment baseline value.
// absFloor guards fast segments; relMultiplier guards slow ones.
function deriveThreshold(baselineP75, { absFloor = 150, relMultiplier = 1.10 } = {}) {
const relative = Math.round(baselineP75 * relMultiplier);
const absolute = baselineP75 + absFloor;
return Math.max(relative, absolute);
}
// Example: LCP thresholds per segment (ms), mid-range mobile on Fast 3G.
const lcpBaselines = {
"home/anon/mobile": 2100,
"plp/anon/mobile": 3200,
"pdp/anon/mobile": 3800,
"checkout/auth/mobile": 4600
};
for (const [segment, p75] of Object.entries(lcpBaselines)) {
console.log(segment, deriveThreshold(p75));
}
// home -> 2310
// plp -> 3520
// pdp -> 4180
// checkout -> 5060
Notice how the derived ceilings differ from any single global number. The home page is held to 2310 ms while checkout is allowed 5060 ms — both on a mid-range mobile device over Fast 3G — because each threshold is anchored to that segment's own reality. A regression that pushes the home page to 2400 ms trips its gate even though 2400 ms would be excellent for checkout. That is the entire point: sensitivity scales to the page, not to the property average.
| Segment key | Baseline LCP P75 (ms) | Gate LCP ceiling (ms) | Baseline INP P75 (ms) | Gate INP ceiling (ms) |
|---|---|---|---|---|
| home/anon/mobile | 2100 | 2310 | 180 | 330 |
| plp/anon/mobile | 3200 | 3520 | 210 | 360 |
| pdp/anon/mobile | 3800 | 4180 | 240 | 390 |
| checkout/auth/mobile | 4600 | 5060 | 320 | 470 |
All the numbers above assume the same device and connection context — a Moto G-class phone throttled to Fast 3G — so that the only variable across rows is the template and audience. When you later add a desktop-over-cable row for the same templates, it becomes a parallel block of segments with its own, much tighter, P75 ceilings; you never compare a mobile ceiling against a desktop measurement. Whether to gate on P75 or the stricter P90 for a given segment is a judgement call the percentile reference works through in detail, but the segmentation stays identical either way.
A CI Matrix That Samples One URL per Template
The CI job becomes a matrix over segments. Each matrix leg runs a synthetic audit against exactly one sample URL, reads that segment's ceiling from the schema, and fails only its own leg. Because every leg is independent, a checkout regression fails the checkout leg and leaves the home-page leg green, which makes the pull-request status check point straight at the responsible template.
In GitHub Actions the matrix reads its legs straight from the segment list. A small step resolves the ceiling for the current segment from the schema and passes it to the audit runner as an assertion.
name: perf-budget
on: pull_request
jobs:
audit:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
segment:
- key: home/anon/mobile
url: https://staging.example.com/
- key: plp/anon/mobile
url: https://staging.example.com/c/shoes
- key: pdp/anon/mobile
url: https://staging.example.com/p/runner-01
- key: checkout/auth/mobile
url: https://staging.example.com/checkout
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Resolve ceiling for segment
id: ceiling
run: |
CEIL=$(jq -r --arg k "${{ matrix.segment.key }}" \
'(.segments[$k].lcpP75 * 1.10 | floor)' baselines.json)
echo "lcp=$CEIL" >> "$GITHUB_OUTPUT"
- name: Run Lighthouse against sample URL
run: |
npx lighthouse "${{ matrix.segment.url }}" \
--preset=perf \
--throttling-method=simulate \
--output=json \
--output-path=result.json \
--chrome-flags="--headless=new"
- name: Assert LCP under segment ceiling
run: |
LCP=$(jq -r '.audits["largest-contentful-paint"].numericValue' result.json)
CEIL=${{ steps.ceiling.outputs.lcp }}
echo "segment=${{ matrix.segment.key }} lcp=${LCP%.*} ceiling=$CEIL"
if [ "${LCP%.*}" -gt "$CEIL" ]; then
echo "::error::${{ matrix.segment.key }} LCP ${LCP%.*}ms over ceiling ${CEIL}ms"
exit 1
fi
fail-fast: false is load-bearing: without it, the first failing leg cancels the others and you lose the full picture of which templates are affected. Keep the sample set to one URL per template. Auditing ten product pages does not make the PDP baseline more trustworthy — it just multiplies runtime and variance, which is why noise reduction and single-representative sampling go hand in hand.
Baseline Inheritance for Brand-New Routes
A new route has no history, so it has no baseline. The wrong reactions are to skip gating it (blind spot) or to gate it against a global number (the original lie). The right reaction is inheritance: a brand-new route provisionally inherits the baseline of the template it renders with, gets audited against that inherited ceiling immediately, and only earns its own measured baseline once it has accumulated enough real-user samples to compute a stable P75. The resolution order is: exact segment key, then same template with a different audience, then the property default as a last resort.
Mark inherited baselines as provisional: true in the schema so a dashboard can distinguish a measured ceiling from a borrowed one, and set a promotion rule that replaces the provisional entry once the segment reaches a sample threshold — 1000 real-user sessions in the trailing window is a reasonable bar for a stable P75. The full mechanics, including how to name the fallback template and when to expire a provisional entry, live in Baseline Inheritance for New Routes.
Troubleshooting Sparse Segments
The recurring problem with segmentation is that some segments never gather enough traffic to compute a trustworthy P75. Checkout on desktop, an authenticated settings page, or a niche locale template can each sit at a few hundred sessions a week, where the P75 jumps around from noise alone. There are four durable fixes, in order of preference.
First, widen the window before you widen anything else. A segment that is too sparse over a trailing 7 days may be perfectly stable over a trailing 28 days. The cost is slower reaction to genuine change, which is usually acceptable for a low-traffic page. Second, relax the percentile. Computing a P75 from 300 samples is far more stable than computing a P90 from the same set; gate a sparse segment at P75 and reserve P90 for high-volume segments where the tail is well populated. Third, fall back one axis. If checkout/auth/desktop is starved, drop the auth axis and use the checkout/*/desktop distribution, which combines authenticated and anonymous checkout — less precise, but populated. Fourth, only as a last resort, merge into the template default and accept that the segment is gated conservatively rather than precisely.
| Symptom | Likely cause | First remedy | Threshold effect |
|---|---|---|---|
| P75 swings run-to-run | Fewer than ~500 samples in window | Widen window to trailing 28d | Slower to react, far more stable |
| Gate flaps green/red on no code change | Gating P90 on a sparse tail | Gate this segment at P75 instead | Fewer false alarms |
| Segment has near-zero traffic | Axis split too fine | Drop one axis (usually auth) | Coarser but populated baseline |
| Brand-new route, no data | No history yet | Inherit template baseline, mark provisional | Gated from first deploy |
Whatever you do, never silently exclude a sparse segment from the gate — that recreates the blind spot segmentation was meant to close. Keep it in the matrix with an inherited or widened baseline and flag it as low-confidence, so a human can see that checkout-on-desktop is being watched loosely rather than not at all. As traffic grows, tighten the window and promote the percentile; the segment graduates from provisional to measured without ever leaving the gate.
Frequently Asked Questions
How many segments should I start with?
Begin with template times device class — for a four-template site on mobile and desktop that is eight segments, each with enough traffic to compute a stable P75. Add the authentication axis only for templates whose logged-in path is genuinely distinct, such as an account dashboard or checkout. Every axis multiplies segment count and divides samples per segment, so add axes deliberately, not reflexively.
Why audit only one URL per template instead of many?
All URLs on a template share a render path, so one representative URL captures the structural cost that CI is meant to protect. Auditing ten product pages multiplies runtime and synthetic variance without making the template baseline more trustworthy. Real-user monitoring covers the full URL population; the CI leg only needs to catch template-level regressions.
What threshold do I use for a route that has no history?
Inherit the baseline of the template the route renders with and gate against that provisional ceiling from the first deploy. Mark the entry provisional in the schema, then replace it with a measured baseline once the segment accumulates enough real-user sessions — around 1000 in the trailing window is a reasonable bar for a stable P75.
How do I keep a sparse segment from producing flaky gate results?
Widen the measurement window first, since a segment that is noisy over 7 days is often stable over 28 days. If it still flaps, gate at P75 rather than P90 because a sparse tail is unreliable, and if traffic is truly minimal, drop one axis so the distribution is populated. Never remove the segment from the gate entirely.
Do segmented baselines replace percentile tuning?
No — they are complementary. Segmentation decides which distributions you are allowed to compare; percentile tuning decides how you summarise each one. You still choose P75 or P90 per segment, but you make that choice on a clean per-template distribution instead of a pooled average that mixes incomparable page types.