Font Performance Monitoring & Auditing: Measuring Web-Font Performance in Lab and Field

Delivering fonts fast is only half the work; the other half is proving it. This guide covers how to measure and audit web-font performance rather than how to deliver it — the instrumentation, tooling, and thresholds that turn "the fonts feel slow" into a number you can defend in a pull request. The audience is frontend engineers and performance specialists who already ship @font-face rules and now need to watch them in production. Three Core Web Vitals are at stake: Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and Interaction to Next Paint (INP). Fonts touch all three, so monitoring starts with measuring font loading performance, continues through debugging font-related layout shift, extends into font loading error handling for the failures your dashboards must also catch, and ends with automated gates via Lighthouse font audits in CI.

The governing principle is that you must measure in two places: the lab and the field. Lab tooling (Chrome DevTools, WebPageTest, Lighthouse) gives reproducible, deeply-instrumented runs on controlled hardware and network profiles — perfect for debugging a specific regression and for CI gating. The field — actual visitors, captured with real user monitoring for web fonts — gives the ground truth that Google ranks on: 28-day rolling 75th-percentile metrics from the Chrome User Experience Report (CrUX). A page can score a perfect 100 in Lighthouse and still fail CLS in the field because real devices are slower, real networks are flakier, and real fonts arrive at unpredictable times. Treat lab metrics as a leading indicator and field metrics as the verdict.

Why Lab and Field Both Matter

Lab measurement is synthetic: you choose the CPU throttle (Lighthouse uses 4x slowdown), the network (Slow 4G / 1.6 Mbps with 150ms RTT), and the viewport. Every run is comparable to the last, which is exactly what you want for catching a regression between two commits. Its weakness is that it is one device, one network, one cold cache — not the full spread of real devices across your audience.

Field measurement is observational: it samples whatever hardware, network, and cache state your users actually have. It captures the partitioned-cache reality (HTTP cache has been keyed by top-level site since Chrome 86 / Firefox 85, so a font on a shared CDN is no longer reused across origins) and the genuine distribution of swap durations. Its weakness is latency — CrUX is a 28-day rolling window, so a regression you ship today may not surface in field data for weeks. That delay is precisely why you also gate in the lab.

A third, often-skipped mode sits between the two: synthetic-with-real-network, i.e. running Lighthouse or WebPageTest against production from multiple geographic vantage points rather than a single CI runner. This catches CDN edge-node problems — a font that is fast from a US-East test but 400ms slower from an Asia-Pacific vantage point because it missed the regional edge cache — that a single-location lab run and even RUM aggregated globally can both mask if your traffic happens to be thin in that region.

Core Web Vitals Impact and the Font-Specific Metrics That Feed Them

Each Core Web Vital has a documented threshold and a font-specific failure mode. Knowing which sub-metric to instrument is the difference between a useful dashboard and a wall of noise.

LCP < 2.5s. When the largest contentful element is a heading or paragraph, its paint cannot complete until the governing font is available (under font-display: block/auto) or until the swap repaints it (under swap). The font-specific input is the font's request-to-usable span: startTimeresponseEnd from PerformanceResourceTiming, plus decode. If a hero font is render-blocking, LCP tracks it almost 1:1. Preloading critical weights via resource hints is the usual remedy, and the monitoring job is to confirm the preloaded font lands before the LCP candidate.

CLS < 0.1. A font swap relays out text when the fallback and web font have different metrics, and every reflowed line contributes a layout-shift value (impact fraction × distance fraction). The font-specific input is the per-shift value from the layout-shift entry, ideally attributed to the swap moment via document.fonts.ready. This is the single most common font-driven CWV failure, and metric overrides — size-adjust, ascent-override, descent-override, line-gap-override on the fallback @font-face — are the fix, designed with help from fallback font stack design.

INP < 200ms. Decoding and shaping a large font on the main thread can block an interaction's event handler from running. The font-specific input is main-thread long tasks that coincide with font parsing — visible in the DevTools Performance flame chart as a "Parse font" task. Oversized or un-subset fonts are the usual culprit; see unicode-range and subset loading for the mitigation.

Core Web Vital Threshold (p75) Font-specific signal Primary tool
LCP < 2.5s Font responseEnd vs LCP time; render-blocking weight WebPageTest, DevTools Performance
CLS < 0.1 layout-shift.value around fonts.ready layout-shift observer, DevTools Rendering
INP < 200ms Long task during font parse/shape DevTools Performance flame chart
(input) FOUT duration track p75 swap start → fonts.ready delta PerformanceObserver, RUM
(input) Font transfer < 800ms p75 responseEnd − requestStart ResourceTiming, RUM

Two edge cases distort this table if you do not account for them. First, a page with no visible text above the fold (an image-first hero, or a canvas-rendered app shell) can pass CLS and LCP cleanly while a font problem still lurks below the fold — audit scroll-triggered shifts separately, since the default layout-shift observer captures them but a dashboard that only samples the first paint window will not. Second, on pages using font-display: optional, the browser may never swap at all if the font misses a roughly 100ms window; in that case your CLS-from-font signal will correctly read zero, but your FOUT-duration metric becomes meaningless because there was no FOUT — track a separate "font never applied" counter instead of interpreting silence as success.

Font Timing Budget vs Measured p75 Bar chart comparing font transfer budget, measured p75 transfer, FOUT duration, and the LCP threshold in milliseconds. Font Timing Budget vs Measured p75 Transfer budget 800ms Measured p75 950ms FOUT duration 300ms LCP threshold 2500ms milliseconds
Measured p75 font transfer already exceeds budget, well inside the LCP threshold.

Architecture Overview: Which Signal to Watch When

Monitoring fails when teams collect everything and look at nothing. The decision is which signal answers which question, and that maps cleanly onto the sub-topics in this section. The matrix below is the routing table: pick the symptom, read the signal, reach for the tool.

Symptom / question Watch this signal Lab tool Field tool Deep-dive
"Text paints late / LCP is high" Font responseEnd, render-blocking status DevTools Performance, WebPageTest filmstrip ResourceTiming + LCP attribution measuring font loading performance
"Layout jumps when the font loads" layout-shift.value near fonts.ready DevTools Rendering → Layout Shift Regions layout-shift observer debugging font-related layout shift
"Did this commit regress fonts?" Lighthouse perf score, resource-size budget Lighthouse CI Lighthouse font audits in CI
"How slow are fonts for real users?" Transfer time p75, FOUT duration p75 RUM beacon, CrUX real user monitoring for web fonts
"Is a glyph stalling on the network?" connectEnd → responseStart per font DevTools Network waterfall ResourceTiming measuring font loading performance
"Did the font fail to load at all?" FontFace.status === 'error', fallback dwell time DevTools Network 404/CORS errors Error-rate beacon font loading error handling

The interconnection is sequential: lab debugging (left column) localizes a problem; CI gating (third row) prevents it from recurring; field RUM (fourth row) confirms the fix reached real users; error monitoring (sixth row) catches the failures that never show up as a slow metric because the font simply never arrived. Most teams build the field and error layers last and regret it — without them you are optimizing a metric Google does not score you on, and you are blind to the subset of users for whom the font pipeline is silently broken rather than merely slow.

Tool Coverage by Core Web Vital Matrix showing which monitoring tools cover LCP, CLS, INP, and font byte budgets. Tool Coverage by Core Web Vital LCP CLS INP Byte budget DevTools yes yes yes no WebPageTest yes yes no yes Lighthouse… yes yes no yes RUM / CrUX yes yes yes no
No single tool covers every metric; lab and field tools are complementary.

Network and Delivery Fundamentals You Must Instrument

Monitoring is only as honest as your understanding of the network path the font travels, so several delivery details from the Font Loading & Delivery Strategies area show up directly in your timing data.

Preload and priority. A correctly preloaded font carries <link rel="preload" as="font" type="font/woff2" crossorigin> — the crossorigin attribute is required even same-origin, and its absence causes a double fetch that you will see as two ResourceTiming entries for the same URL. In your audit, a preloaded-but-double-fetched font is a high-signal red flag. fetchpriority="high" (Chromium) raises the request priority; watch the Network panel's Priority column to confirm it is not stuck at Low.

HTTP/2 and CORS. Multiplexing removes connection setup cost on repeated requests, but the first font on a new origin still pays DNS + TCP + TLS — visible as a long connectEnd in ResourceTiming. Self-hosting (covered under Google Fonts vs self-hosting) collapses that third-party handshake into your own already-warm connection, and the timing delta is exactly what RUM should capture before and after a migration.

Cache partitioning. Because the HTTP cache is partitioned by top-level site, a returning visitor's "cached" font is only cached for that site. Your repeat-view metrics must therefore distinguish first-party cache hits (transfer size ≈ 0, transferSize near zero in ResourceTiming) from cross-site misses. Treating all repeat views as cached is a classic measurement error.

CDN edge and compression. WOFF2 is roughly 30% smaller than WOFF; if your transfer sizes look 30–50% larger than the on-disk WOFF2 file, you are likely serving uncompressed or the wrong format. ResourceTiming's encodedBodySize vs decodedBodySize exposes this directly.

Service workers and offline caching. If a service worker caches font responses (a common pattern for offline-first apps), transferSize will read 0 on a service-worker-served response even on a first visit from a fresh install, which looks identical to an HTTP cache hit in your RUM data. Tag service-worker-served fonts separately by checking entry.workerStart > 0 in the PerformanceResourceTiming entry, or your cache-hit rate will be inflated in a way that is invisible until you try to explain why "cached" font transfer times never seem to correlate with actual network conditions.

Auditing Beyond Slow: Catching Fonts That Fail Outright

A slow font is a performance problem; a font that never loads is a correctness problem that performance dashboards routinely miss because a layout-shift value of 0 and an LCP that resolves against the fallback font both look, superficially, like success. Font loading error handling covers the failure modes in depth — detecting failed font loads and graceful degradation when fonts time out — but the auditing angle deserves its own instrumentation.

Every FontFace object has a status property that transitions unloaded → loading → loaded or error. A CORS misconfiguration, a 404 from a stale CDN path, or a corrupted WOFF2 file all resolve to error, and none of them produce a JavaScript exception you would notice in a generic error tracker — the page simply renders in the fallback font forever, silently. The audit checklist item here is to iterate document.fonts after document.fonts.ready settles (or after a timeout, since ready does not reject on individual failures) and beacon any font whose status is error, tagged with the font's family and the response status if available from a parallel ResourceTiming lookup.

// Detects fonts that failed to load and beacons the failure with enough
// context to reproduce it — family, source URL, and the browser's reported status.
async function auditFontFailures() {
  // fonts.ready resolves once loading settles, but individual failures are
  // silent, so we still need to inspect each FontFace afterward.
  await Promise.race([
    document.fonts.ready,
    new Promise((resolve) => setTimeout(resolve, 4000)), // don't hang forever
  ]);

  const failures = [];
  for (const face of document.fonts) {
    if (face.status === 'error') {
      failures.push({ family: face.family, weight: face.weight, style: face.style });
    }
  }
  if (failures.length) {
    navigator.sendBeacon('/rum/font-errors', JSON.stringify({ failures, ua: navigator.userAgent }));
  }
}
auditFontFailures();

This complements, rather than replaces, the resource-timing observer: transferSize/responseEnd tell you a font was slow, FontFace.status tells you it never worked, and only the combination gives you a complete picture. In practice, treat a nonzero font-error rate as a release-blocking bug on the same severity tier as a CLS regression, not a lower-priority "nice to fix" item — users experiencing it see the fallback font indefinitely, which for icon fonts in particular (see fixing icon font layout shift) can mean missing glyphs rather than merely a different typeface.

Implementation Checklist

Use this checklist to stand up font monitoring from nothing. Each item produces a concrete artifact (a beacon, a budget file, a DevTools step) rather than a vague intention.

  1. Add a PerformanceObserver for type: 'resource' with buffered: true, filter initiatorType === 'css' and name ending in .woff2, and beacon responseEnd − requestStart.
  2. Add a layout-shift observer; correlate each entry's value and startTime against the timestamp of document.fonts.ready.
  3. Record a performance mark at document.fonts.ready (performance.mark('fonts-ready')) and a measure from navigationStart.
  4. Run a baseline Lighthouse audit (lighthouse <url> --preset=desktop and mobile) and store the JSON as the reference.
  5. Create lighthouserc.json with a resourceSizes budget for font and assertions on LCP/CLS; wire it into CI.
  6. In WebPageTest, capture a filmstrip and confirm the LCP frame is not waiting on a font request.
  7. In DevTools → Rendering, enable Layout Shift Regions and reload to see fonts reflow in real time.
  8. Define p75 thresholds: font transfer < 800ms, FOUT duration tracked, CLS contribution from fonts < 0.05.
  9. Add a FontFace.status === 'error' audit pass and beacon failures separately from slow-but-successful loads.
  10. Send field metrics to your RUM endpoint with navigator.sendBeacon on visibilitychange → hidden.
  11. Alert when any p75 threshold regresses across a 7-day window, and alert immediately (not on a rolling window) on any nonzero font-error rate.
Standing Up Font Monitoring Numbered process from adding resource timing instrumentation through to field alerting. Standing Up Font Monitoring 1 Instrument timing resource observer 2 Attribute shift layout-shift + fonts.ready 3 Mark settle time performance.mark 4 Baseline audit Lighthouse 5 Gate in CI lighthouserc.json 6 Alert in field RUM p75, error rate
Six ordered steps take a page from unmeasured to fully gated and alerting.

Auditing and Monitoring Tooling

Lab — Chrome DevTools. The Network panel shows per-font priority, protocol, and the connect/SSL/wait/download breakdown. The Performance panel's flame chart surfaces "Parse font" main-thread tasks (the INP risk). The Rendering drawer's Layout Shift Regions highlights every reflow, letting you watch a font swap shove text in real time.

Lab — WebPageTest. Multi-run, throttled, with a filmstrip and request waterfall. Its "Render Blocking" annotation and the visual-completeness graph make font-driven LCP delay obvious; its repeat-view run validates cache headers.

Lab — Lighthouse / Lighthouse CI. Lighthouse flags missing font-display, render-blocking resources, and oversized payloads, and emits a deterministic score for CI. Lighthouse CI adds assertions and performance budgets so a regression fails the build instead of merely lowering a number nobody reads. Automating font budget checks with Lighthouse CI and setting a font byte budget in Lighthouse cover the exact configuration.

Field — PerformanceObserver and ResourceTiming. PerformanceObserver with type: 'resource' captures every font fetch's timings; with type: 'layout-shift' it captures the shifts a swap causes. PerformanceResourceTiming exposes requestStart, responseEnd, encodedBodySize, and transferSize per font — the raw material for transfer-time and cache-hit analysis. See capturing font timing with the Resource Timing API for a full worked beacon.

Field — RUM and the web-vitals library. Aggregate the above to p75 and segment by connection type and first/repeat view. The web-vitals library's onCLS callback exposes an entries array on each report, and each entry is a raw layout-shift object you can correlate against fonts.ready the same way as the hand-rolled observer above — tracking font CLS with the web-vitals library shows the integration. This is the only layer that reflects cache partitioning and the real device distribution, so it is the metric that ultimately decides whether your optimization worked.

A Worked Audit: Diagnosing a Real CLS Regression

To make the tooling concrete, walk through a representative incident. A deploy ships a new marketing font and CLS in CrUX creeps from 0.06 to 0.14 over the following week — above the 0.1 threshold. The audit proceeds in a fixed order:

  1. Reproduce in the lab first. Open DevTools → Rendering → Layout Shift Regions, hard-reload with cache disabled, and watch for a highlighted region as the page settles. If nothing highlights, the regression may be device- or network-specific (a mid-tier Android phone on a throttled connection reflows visibly where a fast desktop does not), so switch to WebPageTest with a "Moto G Power on 4G" device profile before concluding the lab cannot reproduce it.
  2. Confirm the timing correlation. Add the layout-shift-vs-fonts.ready observer from this guide's code example and check whether the flagged shift's startTime clusters within 100ms of the font settling. If it does not, look at images or third-party embeds before touching fonts at all — a common wasted afternoon is "fixing" a font that was never the cause.
  3. Quantify the metric mismatch. Compare declared metrics (from the font vendor or next/font's automatic metric matching) against measured ones using calculating cap-height for web typography or a tool like Capsize; a mismatch of even 2–3% in x-height between the fallback and web font is enough to shift multi-line paragraphs measurably.
  4. Apply and re-measure, not just re-eyeball. After adding ascent-override/descent-override/size-adjust to the fallback @font-face block, do not just glance at DevTools — re-run the same Lighthouse CI assertion and diff the JSON cumulative-layout-shift numeric value against the pre-fix baseline stored in step 4 of the Implementation Checklist above.
  5. Confirm in the field before closing the incident. Because CrUX lags by up to 28 days, use your own RUM beacon's p75 over the most recent 3–7 days as an early confirmation signal, then close the loop with CrUX once the full window rolls over.

This same five-step shape — reproduce, correlate, quantify, fix-and-remeasure, confirm-in-field — applies to LCP and INP regressions too; only step 2's correlation target changes (render-blocking status for LCP, main-thread long tasks for INP).

Edge Cases That Break Naive Monitoring

Single-page apps and client-side routing. document.fonts.ready resolves once for the whole document lifetime; it does not re-fire when a client-side route change introduces new text using a font weight that was not previously requested. If your SPA lazily requests a bold weight only when a modal opens, you need a per-navigation or per-component timing mark, not a single page-load mark, or you will silently miss every subsequent font-driven shift after the first route.

Iframes and third-party widgets. A PerformanceObserver in the parent frame cannot see resource timing for fonts loaded inside a cross-origin iframe (a payment widget, an embedded video player) due to the same-origin restrictions on the Resource Timing API. If a third-party iframe's font causes a shift in your layout, it will appear as an unattributed layout-shift entry in the parent's observer with no corresponding font-timing entry to correlate it against — document this as a known blind spot rather than chasing a correlation that cannot exist.

Server-side rendering and hydration. With SSR, text is visible before any client JavaScript runs, so a PerformanceObserver registered in a script tag near the top of <head> can miss the earliest font requests if the SSR HTML already contains a <link rel="preload"> that starts fetching before your script parses. Always pass buffered: true (as shown in the code examples above) specifically to cover this gap — it is not an optional nicety for SSR pages, it is required correctness.

Print stylesheets and non-screen media. A @font-face rule scoped to @media print still registers a FontFace in document.fonts, and some monitoring scripts that iterate document.fonts indiscriminately will beacon phantom "slow" fonts that a user viewing the page on screen never actually downloaded, because a browser may defer or skip fetching print-only fonts until a print action occurs. Filter by checking the associated CSSFontFaceRule's media condition, or exclude fonts whose status stays unloaded for the entire session — that is expected for print fonts, not a bug.

Code Configuration Examples

PerformanceObserver resource-timing logger for fonts

// Logs network timing for every web-font fetch, including buffered entries
// that fired before this script ran.
const fontObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!/\.(woff2?|ttf|otf)(\?|$)/.test(entry.name)) continue;
    const transfer = entry.responseEnd - entry.requestStart;
    const fromCache = entry.transferSize === 0 && entry.decodedBodySize > 0;
    navigator.sendBeacon('/rum/font', JSON.stringify({
      url: entry.name,
      transferMs: Math.round(transfer),
      encoded: entry.encodedBodySize,
      decoded: entry.decodedBodySize,
      cached: fromCache,
      protocol: entry.nextHopProtocol,
    }));
  }
});
fontObserver.observe({ type: 'resource', buffered: true });

Layout-shift observer attributing CLS to font swap

// Sums layout-shift values and flags those occurring within 100ms of
// document.fonts.ready as font-attributable.
let fontsReadyAt = Infinity;
document.fonts.ready.then(() => { fontsReadyAt = performance.now(); });

let totalCLS = 0;
let fontCLS = 0;
new PerformanceObserver((list) => {
  for (const shift of list.getEntries()) {
    if (shift.hadRecentInput) continue;       // ignore user-driven shifts
    totalCLS += shift.value;
    if (Math.abs(shift.startTime - fontsReadyAt) < 100) {
      fontCLS += shift.value;                  // attribute to font swap
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    navigator.sendBeacon('/rum/cls', JSON.stringify({ totalCLS, fontCLS }));
  }
}, { once: true });

document.fonts.ready timing mark

// Emit a User Timing measure so the font-settle moment shows up in
// DevTools Performance and in any RUM tool reading the timeline.
performance.mark('fonts-ready-start');
document.fonts.ready.then(() => {
  performance.mark('fonts-ready-end');
  const m = performance.measure('font-settle', 'fonts-ready-start', 'fonts-ready-end');
  // Also relative to navigation start for cross-page comparison:
  console.info('fonts settled at', Math.round(performance.now()), 'ms', '(+', Math.round(m.duration), 'ms)');
});

Lighthouse CI budget config (lighthouserc.json)

{
  "ci": {
    "collect": {
      "numberOfRuns": 3,
      "url": ["https://example.com/"],
      "settings": { "preset": "desktop" }
    },
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "font-display": "error",
        "resource-summary:font:size": ["error", { "maxNumericValue": 150000 }],
        "resource-summary:font:count": ["warn", { "maxNumericValue": 4 }]
      }
    },
    "upload": { "target": "temporary-public-storage" }
  }
}

Performance budget JSON for Lighthouse (budget.json)

[
  {
    "path": "/*",
    "resourceSizes": [
      { "resourceType": "font", "budget": 150 }
    ],
    "resourceCounts": [
      { "resourceType": "font", "budget": 4 }
    ]
  }
]

Common Pitfalls

  • Measuring only in the lab. A 100/100 Lighthouse run on a fast machine hides field CLS from slow devices. Always pair lab gating with RUM p75 from real users.
  • Trusting repeat-view "cached" numbers across origins. Cache partitioning means a CDN font is not reused across sites; counting all repeat views as cache hits overstates real-world speed. Segment on transferSize === 0.
  • Ignoring buffered entries. A PerformanceObserver created after fonts have already loaded misses them entirely unless you pass buffered: true. Font fetches start early, so this silently drops most of your data.
  • Attributing all CLS to fonts. Layout shift also comes from images and ads. Without correlating shift startTime to document.fonts.ready, you will "fix" fonts and watch CLS stay flat.
  • Counting user-initiated shifts. Omitting the hadRecentInput check inflates CLS with shifts that CWV explicitly excludes, producing alerts on healthy pages.
  • Blocking on document.fonts.ready for measurement and rendering. Using the promise to gate hydration degrades INP and TTI; use it to mark timing, not to delay interactivity.
  • Budgeting total bytes but not font bytes. A global resource budget can pass while a single un-subset font balloons to 300KB. Set an explicit per-font resource budget (target < 150KB total, < 50KB per subset).
  • No throttling in CI. Running Lighthouse CI on un-throttled CI hardware produces optimistic LCP that never matches the field; pin the preset and CPU/network throttle so runs are comparable.
  • Treating a zero-CLS report as proof of success. As shown above, font-display: optional pages and print-only @font-face rules can legitimately report zero font-driven CLS without meaning your loading strategy is optimal — check the font-error and never-applied counters alongside CLS, not instead of it.
  • Confusing service-worker cache hits with HTTP cache hits. Both report transferSize: 0; conflating them will make your first-visit cache-hit rate look implausibly high until you check workerStart.

Frequently Asked Questions

Should I gate CI on lab metrics or field metrics? Gate on lab metrics, because they are deterministic and available on every commit; field metrics arrive on a 28-day delay and vary with traffic, so they cannot block a merge. Use Lighthouse CI assertions (LCP < 2.5s, CLS < 0.1, a per-font size budget) as the gate, and treat CrUX p75 as the post-deploy verdict that confirms the lab gate is calibrated correctly.

How do I prove a layout shift was caused by a font and not an image? Record the timestamp of document.fonts.ready, then in your layout-shift observer flag any entry whose startTime falls within ~100ms of that mark. In DevTools, enable Rendering → Layout Shift Regions and reload: font-driven reflows appear at the moment text restyles, visually separating them from image-driven shifts that occur as images decode.

Why does PerformanceObserver miss my fonts? Almost always because the observer was created after the fonts finished loading and you did not request buffered entries. Pass { type: 'resource', buffered: true } so the observer replays entries from the performance buffer. Also confirm your URL filter matches the actual font extension and any query string, and remember that fonts served from a cross-origin iframe are invisible to a parent-frame observer regardless of buffering.

What font transfer time should trigger an alert? A common threshold is 800ms at the 75th percentile for a critical font's responseEnd − requestStart. Alert when the rolling 7-day p75 crosses it. Segment by connection type and by first vs repeat view, since cache partitioning means many "repeat" visitors still pay full transfer cost on a different origin.

Do I still need WebPageTest if I run Lighthouse CI? Yes, for different jobs. Lighthouse CI is the automated gate that runs on every commit; WebPageTest is the deep diagnostic you reach for when the gate fails and you need a filmstrip, a full request waterfall, and multi-run variance to localize a font-driven LCP delay. They are complementary, not redundant.

How do fonts affect INP, and how do I measure it? A large font decoded or shaped on the main thread can block an interaction's handler, pushing INP past 200ms. Measure it by opening the DevTools Performance flame chart during an interaction and looking for a "Parse font" long task overlapping input; subsetting the font (target < 50KB per subset) and avoiding mid-interaction font loads is the fix.

How do I monitor fonts that fail silently instead of loading slowly? Iterate document.fonts after fonts.ready settles (or after a fixed timeout, since ready never rejects for individual failures) and beacon any FontFace whose status is error. Treat a nonzero error rate as release-blocking, not as a lower-priority slow-loading issue, since affected users see the fallback font indefinitely rather than a delayed web font.

Do single-page apps need different font monitoring than multi-page sites? Yes. document.fonts.ready only resolves once for the document's lifetime, so a client-side route change that lazily requests a new font weight will not produce a second ready event. Add a per-navigation timing mark (for example, on your router's afterEach hook) that checks document.fonts.check(...) for the weights the new route needs, rather than relying solely on the initial page-load promise.

Related