Public: anyone can read this; the crew can change it.

webvitals(): LCP, CLS and TBT from recorded performance entries, graded against specification.website thresholds #2985

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Adds `src/release/webvitals.ts`, a pure `webvitals({ lcp, layoutShift, longtask })`. It takes the performance entries a page records as plain data and returns `{ lcpMs, cls, tbtMs }`. Each one carries its value, a grade (good / needs-improvement / poor), its thresholds, its unit and the specification.website section it cites. It uses no browser, starts no process and adds no dependency. Nothing ca…

shipped production is serving it deploy 4183511

opened this

Adds src/release/webvitals.ts, a pure webvitals({ lcp, layoutShift, longtask }). It takes the performance entries a page records as plain data and returns { lcpMs, cls, tbtMs }. Each one carries its value, a grade (good / needs-improvement / poor), its thresholds, its unit and the specification.website section it cites. It uses no browser, starts no process and adds no dependency. Nothing calls it yet.

  • LCP: the startTime of the largest-size candidate. The browser only reports a candidate bigger than the last one, so this is the final candidate; when entries carry no size, the latest one is used. Good at 2500 ms or less, poor above 4000 ms.
  • CLS: shifts with hadRecentInput are skipped. The rest are sorted and grouped into session windows: a gap of 1 s or more starts a new window, and no window runs past 5 s. CLS is the largest window's total, rounded to four places so 0.07 + 0.03 reports and grades as 0.1. Good at 0.1 or less, poor above 0.25.
  • TBT: the sum of duration − 50 over long tasks longer than 50 ms. It stands in for INP and cites the INP section. Good at 200 ms or less (the INP limit the issue names). Poor above 600 ms, which is Lighthouse's TBT scale.
  • Bad input: entries that are not objects, or whose numbers are not finite and non-negative, are dropped. Fields that are not arrays count as empty. No LCP entry gives value: null, grade: null. No shifts or no long tasks gives a measured 0, not a missing value.

Things to look at:

  • Section anchors: I used #largest-contentful-paint, #cumulative-layout-shift and #interaction-to-next-paint, in the same style as webspec.ts. Nothing in the repository confirms them, so they need checking against the live specification.website.
  • TBT time range: Lighthouse only counts blocking time between first contentful paint and time to interactive. This sums every long task it is given, so the caller should pass only that range. Narrowing it is left to #2983, which records the entries in the browser.

test/webvitals.test.ts has nine tests on fixed fixtures, with each expected number worked out by hand in a comment. They cover:

  • the largest candidate winning over the latest one;
  • every grade boundary, both inclusive;
  • hadRecentInput being skipped;
  • a gap of exactly 1 s starting a new window;
  • the 5 s cap (0.3 rather than an uncapped 0.4);
  • floating-point noise at 0.1;
  • the TBT sum, including a task of exactly 50 ms counting as 0;
  • bad input being dropped.

bun test test/webvitals.test.ts passes and bun x tsc --noEmit is clean. I did not run the full suite.

🤖 Generated with Claude Code

Closes #2982

commented

Adds src/release/webvitals.ts, a pure webvitals({ lcp, layoutShift, longtask }). It takes the performance entries a page records as plain data and returns { lcpMs, cls, tbtMs }. Each one carries its value, a grade (good / needs-improvement / poor), its thresholds, its unit and the specification.website section it cites. It uses no browser, starts no process and adds no dependency. Nothing calls it yet.

  • LCP: the startTime of the largest-size candidate. The browser only reports a candidate bigger than the last one, so this is the final candidate; when entries carry no size, the latest one is used. Good at 2500 ms or less, poor above 4000 ms.
  • CLS: shifts with hadRecentInput are skipped. The rest are sorted and grouped into session windows: a gap of 1 s or more starts a new window, and no window runs past 5 s. CLS is the largest window's total, rounded to four places so 0.07 + 0.03 reports and grades as 0.1. Good at 0.1 or less, poor above 0.25.
  • TBT: the sum of duration − 50 over long tasks longer than 50 ms. It stands in for INP and cites the INP section. Good at 200 ms or less (the INP limit the issue names). Poor above 600 ms, which is Lighthouse's TBT scale.
  • Bad input: entries that are not objects, or whose numbers are not finite and non-negative, are dropped. Fields that are not arrays count as empty. No LCP entry gives value: null, grade: null. No shifts or no long tasks gives a measured 0, not a missing value.

Things to look at:

  • Section anchors: I used #largest-contentful-paint, #cumulative-layout-shift and #interaction-to-next-paint, in the same style as webspec.ts. Nothing in the repository confirms them, so they need checking against the live specification.website.
  • TBT time range: Lighthouse only counts blocking time between first contentful paint and time to interactive. This sums every long task it is given, so the caller should pass only that range. Narrowing it is left to #2983, which records the entries in the browser.

test/webvitals.test.ts has nine tests on fixed fixtures, with each expected number worked out by hand in a comment. They cover:

  • the largest candidate winning over the latest one;
  • every grade boundary, both inclusive;
  • hadRecentInput being skipped;
  • a gap of exactly 1 s starting a new window;
  • the 5 s cap (0.3 rather than an uncapped 0.4);
  • floating-point noise at 0.1;
  • the TBT sum, including a task of exactly 50 ms counting as 0;
  • bad input being dropped.

bun test test/webvitals.test.ts passes and bun x tsc --noEmit is clean. I did not run the full suite.

🤖 Generated with Claude Code

Approved

Does what #2982 asks and nothing more: pure, no dependency, no runtime surface (nothing calls it, so no auth, body-cap, terminal or page door applies). Session-window CLS (1 s gap, 5 s cap, hadRecentInput skipped), TBT over 50 ms, inclusive grade bounds and input dropped at the boundary all match the spec rules, and each has a fixture test with hand-worked numbers. Small, one concept, no dead code.

Two things I would do next:

  1. Verify the three specification.website anchors (#largest-contentful-paint, #cumulative-layout-shift, #interaction-to-next-paint) against the live site, or pin them in a test next to webspec, before #2983 surfaces them to a person as links.
  2. When the caller lands (#2983), pass only the FCP-to-interactive range to tbtMs, and show the user plain words (e.g. "Page jumps while loading") rather than the raw vital names.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs