koala73 / koala73/worldmonitor

perf(CLS): above-the-fold instability on /dashboard — skeleton→app swap + #panelsGrid/#main displacement; desktop regressing 0.10→0.14 (#4487)

Open
#4,580 13 comments 0 reactions 0 assignees View on GitHub
area:frontend claude enhancement P2 performance platform: mobile
Dominant language
TypeScript
Stars
86.6k
Forks
13.1k
Avg merge
8h 4m
Merged PRs (30d)
825

Description

**KPI: page-level CrUX for `/dashboard` — CLS p75 desktop 0.14 / 63% good (**regressing**: 0.10→0.13→0.14 over weeks ending 6/20→7/4), mobile 0.16 / 69% good (improving: 0.22→0.16). Target ≤0.1 p75 / ≥75% good on both.**

## TL;DR

Attribution is **done** (instrumentation shipped in #4585 → #4652 → #4903; verdict landed 2026-07-09). The bad shifts are **above-the-fold instability at scrollY≈0**, three named mechanisms: **(a)** boot skeleton→app swap footprint mismatch, **(b)** whole-grid displacement of `#panelsGrid`/`#main` (something above/inside them resizes late), **(c)** country-deep-dive population. Background-tab-reveal and scroll-swap are **disproven** (§Disproven). **Fixes shipped 2026-07-09/10** — #5124 (deep-dive motion → transform), #5133 (mobile skeleton↔map footprint parity), #5137 (cached-mode banner out of flow — the dominant above-grid shover). Now in **field verification**; §2 tracks residuals. Watch the **desktop** line first: it was the one actively regressing.

## 1. Evidence (2026-07-09)

| Source | What it measures | Finding |
|---|---|---|
| CrUX page-level, 28d ending 07-07 | official pass/fail; 28d lag | desktop p75 0.14 (63% good, **worsening 3 straight weeks**); mobile 0.16 (69% good, improving) |
| DebugBear RUM (live since 07-09; full population) | live p75 by shift element | see table below |
| Sentry `webvital:cls` (bad-tail only, value ≥0.1 by design #4565) | attribution depth | n=1189 (07-09) + n=100 (07-06 cross-check, identical): value p50 0.191 / p75 0.344; **hiddenAtLoad 0.4%**, hadHiddenPeriod 1.6%, **scrollY ≤10px in 100%** of bad events |

**DebugBear day-1, p75 of `clsValue` by `clsSelector` (full population):**

| element | n | p75 | device |
|---|---|---|---|
| `#main div#panelsGrid` | 89 | **0.367** | desktop |
| `#main div#panelsGrid` | 20 | **1.039** | mobile |
| `#app div.skeleton-map-content` | 12 | **0.811** | desktop |
| `#app main#main` | 29 / 10 | 0.388 / 0.728 | desktop / mobile |
| `#panelsGrid div.panel` | 285 | 0.167 | desktop |
| `.wm-pro-banner-reserved aside#country-deep-dive-panel` | 12 | 0.247 | desktop |
| `#mapSection div#mapBottomGrid` | 14 | 0.241 | desktop |
| `#deep-dive-content div.cdp-grid` | 26* | 0.568* | *Sentry bad-tail figure |

Sentry's bad-tail target ranking agrees: `#panelsGrid` (n=184, shift p75 0.554), `#main` (103, 0.486), `skeleton-map-body` (20, **0.740**), `skeleton-panel` (17, 0.608), `panel-deferred-shell.span-2` (103, 0.169), `#country-deep-dive-panel` (27, 0.497).

## Disproven — do NOT re-propose (each cost a full investigation)

- **Background-tab-reveal**: hiddenAtLoad 0.4% / hadHiddenPeriod 1.6% (the #4903 extras were built to test exactly this). NOTE: `visibilityState=hidden` reads ~100% on CLS events — that's a **flush artifact** (CLS reports on visibilitychange→hidden), never evidence.
- **Per-panel first-arrival row growth**: grid rows pinned at 200px (span-2 404px) through data arrival; lab CLS ≈0.003.
- **Scroll-swap shell shifts**: full scroll pass = zero layout-shift entries; #4652's footprint parity holds. Also explains why lab CLS (0.001) never reproduced field: the shifts need real data/timing, and headless can't emulate tab visibility anyway.
- **Below-fold swap**: scrollY >500px in 0% of bad events.
- **Async panel-mount displacement (2026-07-10 theory)**: throttled lab (Slow-4G + 4× CPU, buffered layout-shift observer) shows ZERO in-grid `[data-panel]` shifts on boot, desktop or mobile — deferred shells absorb lazy mounts, and `applySavedPanelOrder()` is NOT on the boot path (user tab actions only). Field `#panelsGrid`/`.panel` rank measured the moved victim, not the cause. Lesson: the shover (a removed banner) never appears in `largestShiftTarget`.

## 2. Remaining work

- [x] **(a) Skeleton↔app footprint parity — SHIPPED (PR #5133)**: desktop parity was already clean; mobile `.skeleton-map` had no mobile override (50vh vs the real full-viewport map = +374–464px under-reservation shoving #panelsGrid on swap) + a 767-vs-768 breakpoint seam (iPad portrait fully de-synced). Fixed in index.html inline CSS; guard = `tests/skeleton-app-footprint-parity.test.mjs`. RESIDUAL: the map-collapsed cohort now OVER-reserves — clean fix = stamp `html.wm-map-collapsed` from the inline boot script (same pattern as `wm-pro-banner-reserved`).
- [x] **(b) Stabilize above-grid elements — DOMINANT CAUSE SHIPPED (PR #5137)**: proof-first re-diagnosis (throttled Slow-4G + 4× CPU, buffered layout-shift observer) showed the earlier "async panel-mount displacement" theory DOES NOT reproduce (see §Disproven) — the grid rows in field attribution are the moved VICTIM. Real dominant shover = `.cached-mode-banner` (App.ts:361-388; 83px in-flow, inserted after .header on slow connections, cached→live toggle; removed elements never appear as largestShiftTarget). Fixed: `position:fixed;bottom:0;pointer-events:none` + guard test. RESIDUALS: intermittent desktop `header-right` (0.064) + `#panelTabsMount` (0.027) — timing-dependent, deferred pending field data; the >10s late tail (AI Market Implications / Live News re-renders without reserved heights, shift p75 0.574) still unowned.
- [ ] **(c) Deep-dive population containment** — `#country-deep-dive-panel`/`.cdp-grid` shifts on open (0.25–0.57); reserve/contain before populate.
- [ ] **Desktop-regression check** — the worsening started in the weeks ending 6/20→7/4; once (a)/(b) land, if desktop doesn't recover, bisect the merges in that window.
- [ ] Verify per §4.

## 3. Tooling (all live)

- **Sentry**: issue group `webvital:cls`, tag `formFactor` (live since PR #5090), tag `cls.rating`; per-event extras in top-level `context`: `hiddenAtLoad`, `hadHiddenPeriod`, `scrollY`, `viewport`, `largestShiftTarget`, `largestShiftValue`. Bad-tail only — judge by **rate**, not p75.
- **DebugBear aggregates**: `GET https://www.debugbear.com/api/v1/project/103025/rumMetrics?from=&to=&groupBy=clsSelector&device=desktop` with header `x-api-key: $DEBUGBEAR_API_KEY` (key in `.env.local`; base is `www.debugbear.com/api/v1` — `api.debugbear.com` doesn't resolve). Per-view rows: `.../rumPageViews?...` → `clsValue/clsSelector/clsText/clsScrollTop/clsStartTime/clsMainValue`.
- **CrUX official**: `records:queryHistoryRecord` for the `/dashboard` URL per form factor (`GOOGLE_API_KEY` in `.env.local`).

## 4. Verification loop

DebugBear `clsSelector` p75s (full population, daily-capable) → Sentry bad-event rate per target → CrUX page-level weekly. Healthy signal: `#main div#panelsGrid` and the skeleton selectors drop first, desktop %good stops falling. Pass = p75 ≤0.1 / ≥75% good both form factors.

---
Part of #4487 (priority 2, after mobile INP #5080). Filed 2026-07-01 as "untracked field CLS" (instrumentation phase); attribution shipped via #4585, #4652, #4903; verdict 2026-07-09; body consolidated 2026-07-09 — original filing and investigation history in the edit history and comments below.

Contributor guide

Open the contributing guide

Research direction

Start with the remaining-work checklist, especially the country deep-dive population path and the desktop-regression check; App.ts lines 361-388 and the inline CSS in index.html document already-investigated causes and shipped fixes. Use the DebugBear CLS aggregates, Sentry bad-event rates, and weekly CrUX data to verify that deep-dive and residual shifts are contained and that both form factors reach p75 ≤0.1 or ≥75% good.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend, observability, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.