koala73 / koala73/worldmonitor
perf(pwa): service worker registers on the boot path — 882KB High-priority precache wave lands inside the paint window
- Dominant language
- TypeScript
- Stars
- 86.6k
- Forks
- 13.1k
- Avg merge
- 8h 4m
- Merged PRs (30d)
- 825
Description
## Problem
The service worker registers on the **critical boot path**, so Workbox's precache wave (74 requests / **882 KB**, all `High` priority) is issued *during* first paint instead of after it. On the DebugBear mobile profile (US East, 1.6 Mbps, 150 ms RTT, slow device, Chrome 147, 2026-07-18 07:05 UTC) the wave spans **6.7 s → 34.9 s** — straight through the visual-completion window (Visually Complete **24.1 s**, TTI **35.3 s**).
At the tested 1.6 Mbps (~200 KB/s) those 882 KB are **~4.4 s of bandwidth** taken from the app's own critical path, on a page whose total transfer is 3,638 KB. The precache is **24.2 %** of everything the page downloads.
## Root cause
`src/main.ts:515` (origin/main `c45acd49c`) registers at module-eval time, inside the top-level guard at `src/main.ts:491` — nothing gates it on `load` or idle:
```js
if (!('__TAURI_INTERNALS__' in window) && !('__TAURI__' in window) && 'serviceWorker' in navigator) {
installSwUpdateHandler({ version: __APP_VERSION__ });
...
navigator.serviceWorker.register('/sw.js', { scope: '/' }) // ← line 515, boot path
```
`main-D47XApCo.js` finishes evaluating at ~6.7 s on this profile, and the first precache fetch leaves at **6,720 ms** — the wave starts the instant `main.js` runs.
## Evidence
Source: DebugBear analysis **[83831737](https://www.debugbear.com/viewResult/83831737)** (project `103025`, page `693052`) — `www.worldmonitor.app/dashboard`, 303 requests, test config **US East, 1.6 Mbps, 150 ms RTT, slow device, Chrome 147, Lighthouse 13.4.0**, run **2026-07-18 07:05 UTC**. Day-over-day comparison baseline is **[83766301](https://www.debugbear.com/viewResult/83766301)** (2026-07-17 07:03 UTC).
> Trend figures (29 samples, 2026-06-30 → 2026-07-18) come from the series export for the same page; each sample carries its own `analysis.id`, so any row is re-openable at `debugbear.com/viewResult/`.
| | value |
|---|---|
| SW precache requests for `/assets/*` | 111 |
| …already loaded by the app (0-byte HTTP cache hit) | 36 |
| …**net-new bytes off the network** | **74 requests, 882 KB** |
| window | 6.7 s → 34.9 s |
| priority | `High` (71/71 of the post-25 s tail) |
| share of total page transfer (3,638 KB) | **24.2 %** |
Largest net-new items — none of which this page executed:
```
277.7 KB start=32,175ms maplibre-DfDOI80j.js
78.7 KB start=34,277ms main-BU6Lo6Ic.css
39.9 KB start=29,567ms panels-economy-CIhfp2uk.js
36.1 KB start=28,724ms panels-markets-Di7EvD6u.js
31.0 KB start=28,363ms panels-risk-BToxMI9N.js
25.8 KB start=29,229ms panels-energy-BJU2dFhz.js
20.8 KB start=30,022ms panels-defense-Dziz3eM6.js
```
Checked and **ruled out**: these are not double-downloads. Of the 64 URLs requested more than once, 63 second-occurrences transfer 0 bytes (HTTP cache); only 17 KB is true re-download. The cost is the 882 KB of chunks the app never needed, not redundancy.
## Correction to #5165
#5165 attributes its "wave 2" (26.0–31.5 s: panels-markets / -economy / -risk / -energy / -defense) to **the app deferring panel chunk loads**. On this trace that attribution is wrong — every one of those chunks was requested **only** as `resourceType: fetch` at `High` priority and **never** as `script`:
```
panels-economy-CIhfp2uk.js fetch High start=29,567ms 39.9KB
panels-markets-Di7EvD6u.js fetch High start=28,724ms 36.1KB
panels-risk-BToxMI9N.js fetch High start=28,363ms 31.0KB
panels-energy-BJU2dFhz.js fetch High start=29,229ms 25.8KB
panels-defense-Dziz3eM6.js fetch High start=30,022ms 20.8KB
products-B3ABKoc4.js fetch High start=27,685ms 3.3KB
```
The app never executed them. That network wave is **Workbox precaching**, not deferred panel hydration. The long tasks #5165 measures may still be real, but the fetches they were aligned against have a different owner — worth re-checking before changing defer boundaries there.
## Proposed fix
Gate registration on `load` + idle so the precache wave lands after the paint window. Changes **when**, not **what** — no change to what ends up cached, so no offline/update-semantics risk:
```js
const registerSw = () => navigator.serviceWorker.register('/sw.js', { scope: '/' }).then(...);
if (document.readyState === 'complete') scheduleIdle(registerSw);
else addEventListener('load', () => scheduleIdle(registerSw), { once: true });
```
(with a `setTimeout` fallback where `requestIdleCallback` is unavailable — Safari).
Keep `installSwUpdateHandler` where it is if it must observe early lifecycle events; only the `register()` call needs deferring.
## Scope
- Independent of #5373, which narrows `globPatterns`: that one changes *what* is precached, this one changes *when*. Both are worth doing; neither is a dup of the other.
- Related: #5165 (post-hydration long tasks — see correction above), #4578 (closed; off-page precache scope).
- Do not regress the now-passing field LCP/CLS path: CrUX p75 over 30 Jun → 18 Jul improved LCP 3,761 → 1,503 ms and CLS 0.18 → 0.09.
- Related: #4487 (parent CWV epic that #5165 sits under). Filed as a sibling rather than a child: #4487 tracks *field* Core Web Vitals, and this is first-visit bandwidth showing up in lab Speed Index / TTI — reparent if you'd rather track it there.
## Acceptance
- First precache fetch is issued after `load` + idle, verified in a fresh-profile waterfall (no `/assets/*` `fetch` before Visually Complete).
- Re-run the same DebugBear mobile profile; record Speed Index, Visually Complete, TTI, and total transfer before/after.
- Confirm the SW still installs, precaches the same manifest, and `skipWaiting`/`clientsClaim` update behaviour is unchanged (offline second visit still works).
- Note run-to-run noise when reading results: TBT on this profile has **CV 28 %** across 29 samples (2,969–8,756 ms) — do not judge this change on a single-run TBT delta.
Contributor guide
Research direction
Start in src/main.ts around the guard at line 491 and the service-worker registration at line 515. Defer registration until load plus idle, then verify with a fresh-profile waterfall that precaching begins after visual completion. Re-run the specified DebugBear mobile profile and confirm the same manifest, update behavior, and offline second visit remain intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100