Calipers

← The sequence

Layout shift with the guilty element named

03of 09after 02614 wordsINSTALL_SHIFT_OBSERVER

The failure it prevents

CLS 0.31 is a number nobody can act on. The team sees it in a Lighthouse run, agrees it is bad, and does nothing, because finding which of forty components grew is an afternoon of bisecting. Meanwhile the actual cause — a demo card that animates its own height while it plays — shoves every section below it down the page mid-scroll, which is the single most visible way a page reads as unfinished.

The prompt

Add a layout-shift gate to the browser harness that never reports a bare score. Every failure must name the element that moved and print its geometry before and after.

**Install the observer before the page's own code runs.** Via `page.evaluateOnNewDocument` (not `page.evaluate` after navigation — you will miss every shift during hydration):

```js
window.__shifts = [];
const describe = (node) => {
  if (!node || node.nodeType !== 1) return "(unknown)";
  const parts = [];
  for (let el = node; el && el.nodeType === 1 && parts.length < 4; el = el.parentElement) {
    let s = el.tagName.toLowerCase();
    if (el.id) s += `#${el.id}`;
    else if (typeof el.className === "string" && el.className.trim())
      s += "." + el.className.trim().split(/\s+/).slice(0, 2).join(".");
    parts.unshift(s);
  }
  return parts.join(" > ");
};
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.hadRecentInput) continue;
    window.__shifts.push({
      value: entry.value,
      sources: (entry.sources ?? []).map((s) => ({
        node: describe(s.node),
        from: s.previousRect ? { y: s.previousRect.y, h: s.previousRect.height } : null,
        to:   s.currentRect  ? { y: s.currentRect.y,  h: s.currentRect.height  } : null,
      })),
    });
  }
}).observe({ type: "layout-shift", buffered: true });
```

`buffered: true` is load-bearing — without it you lose everything that shifted before the observer attached. The four-ancestor walk is what turns a score into a bug report. If your app sets a stable test attribute on components, prefer it over the class name inside `describe` — minified class names name nobody.

**Measure three times, with three different instruments, because each one fails differently.**

1. *Proof of life.* After the stepped read-through, sum `window.__shifts` and record it as a **fact, not a check** — `route.clsWhileScrolling`. It exists so that a zero from the real check below cannot silently be a dead observer.
2. *The check.* Scroll back to top, reset `window.__shifts = []`, record `document.documentElement.scrollHeight`, wait `PLAY_MS`, then read the shifts. Default `PLAY_MS = 6000` — long enough for one loop of the longest looping animation in this app. **Make it a flag and write the reason in a comment**: a 12-second hero under-measures at 6s, and a fully static page wastes 6s per route. Fail when total CLS exceeds `CONTRACT.clsBudget` (default 0.01 — ten times stricter than Google's 0.1, and the failure message must say why: if entrances are transform/opacity only, anything above measurement noise is a real reflow, not a budget being spent).
3. *The cross-check.* Compare `scrollHeight` before and after the wait. If it changed by more than 4px, fail independently — a page that grew with no interaction has a component animating its own box. This instrument does not depend on the observer at all, so it survives the observer being wrong.

**The failure message is the product.** Sort the shifts descending, take the worst three, and print for each:

```
FAIL  layout-shift  /pricing · CLS 0.184
      the page moves while its content settles
      0.1120 — div#root > main > section.pricing > div.card
              height 100 → 400, y 0 → 0
```

A failure that prints only `CLS 0.184` has not done the job — send it back.

**Do not** compute your own shift score from rect diffs. Use Chrome's `layout-shift` entries: they are the same numbers that decide the site's real CLS, so the gate and the field metric cannot disagree.

**Also record, per route, into the JSON facts bag:** `cls`, `clsWhileScrolling`, and the document height delta. These are the numbers you will want to diff between two runs when someone asks whether last week's refactor helped.

**Hand back:** the probe, the check, and a run against this project printing the per-route CLS facts. If every route reports 0, say explicitly that this number is only believable once the probe self-test exists — and point at that as the next task.

What it produces

An INSTALL_SHIFT_OBSERVER probe plus a layout-shift check that fails with the culprit's ancestor path and its from→to geometry, backed by three independent instruments and a per-route facts entry.

How you prove it works

Serve a fixture page: <div id="grow" style="height:100px"> followed by a 2000px spacer, and a script that sets #grow to 400px after 800ms. The check must report CLS > 0, must name div#grow, and must print height 100 → 400. Then the control: the identical page with the timeout removed must report exactly 0 — a probe that reports shift on a static page will be muted by the team within a week. Then the third instrument on its own: a page that appends a 300px block after load must fail the document-height check even if you deliberately stub the observer out.