koala73 / koala73/worldmonitor

perf(pwa): service worker registers on the boot path — 882KB High-priority precache wave lands inside the paint window

Open
#5,372 0 comments 0 reactions 0 assignees View on GitHub
area:frontend enhancement performance
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.