Repository navigation
Conversation
After every deploy, the first two loads of an app that already had a service worker could stall before Flutter started. The first load registers the new build, which starts installing while the previous worker still controls the page. waitForActivation then waited for the new worker to become active, which cannot happen while the page is controlled, so the wait always ran its full 4s before the page booted with the old controller anyway. The next load finds the new worker waiting and activateWaitingAtBootstrap posts skipWaiting. Usually the handover lands in milliseconds. But activation stops the old worker, and a request the page makes at that moment (the browser's favicon fetch, a logo, the web manifest) is still dispatched to it, which restarts it; Chromium then keeps the new worker waiting for as long as the page is open, and re-sending skipWaiting does not help. The bootstrap waited 4s for a controllerchange that could not come, then 4s more in waitForActivation, again ending with the old controller. - A controlled page with a newer worker installing or waiting now boots with its current controller at once. The newer worker takes over when the page closes, and sw-update-available still announces it. First visits (no controller) wait for activation as before. - The handover is bounded by SW_HANDOFF_TIMEOUT_MS (500ms). In both cases the page ends up exactly where it did before; only the waiting is gone. Tests: Vitest covers a controlled page with an installing worker returning without waiting, a lost handover resolving after the new bound, and an uncontrolled page still waiting for activation; all three fail without the fix. The full Vitest, Dart and Playwright suites pass. Measured on the example app with a local server and repeated deploys in Chromium 152: the first load after a deploy went from 4.3-5.0s every time to 0.31-0.34s, and a lost handover race from 8.3s to 0.84s.
|
Thanks — #24 is merged, so this now conflicts on CI on this branch merged with |
|
An update before you rebase: part of this PR now overlaps with #28. #28 deals with the first load after a deploy more broadly. The worker now reports its build's
So once #27 → #28 → #29 land, the Could you rebase onto |
What happened
After every deploy, the first two loads for a returning visitor stalled in the bootstrap before Flutter started:
Activation timed out, continuing without SWWaiting-worker handoff timed out, continuing with old, thenActivation timed out, continuing without SWIn both cases the page then booted with the previous controller, which is the outcome it could have reached immediately.
Root cause
1st load.
register()starts installing the new build while the previous worker still controls the page.waitForActivationthen waits for the new worker to become active. A worker can't activate while its predecessor still controls a page unlessskipWaitingis called, so the wait always runs the fullSW_REGISTRATION_TIMEOUT_MS.2nd load. The new worker is waiting, and
activateWaitingAtBootstrappostsskipWaiting. Usually the handover lands within milliseconds. But activation stops the old worker, and a request the page makes at that moment (the browser's favicon fetch, a logo, the web manifest) is still dispatched to the old worker, which restarts it. Chromium then keeps the new worker waiting for as long as the page stays open: thecontrollerchangenever comes, re-sendingskipWaitingdoesn't help, and activation happens only when the page unloads.This was reproduced with the example app, a local static server and repeated deploys, recording worker status changes through the DevTools protocol:
favicon.pngrequested →skipWaiting→ old workerstopped→redundant→ new workeractivating.skipWaiting→favicon.pngrequested in the same millisecond → old workerstopped→starting→running→ no further change for the new worker until the page is navigated away from.The fix
sw-update-availablestill announces it. First visits (no controller) wait for activation as before.SW_HANDOFF_TIMEOUT_MS(500 ms), since a handover that works lands in milliseconds. After that, a controlled page boots with its current controller.sw-update-available→applyUpdate) is unchanged.bootstrap_template.dartis regenerated. README anddocs/bootstrap-pipeline.mddescribe the new behaviour, and there's a CHANGELOG entry under Unreleased.Verification
Vitest: 275 pass. There are new tests for a controlled page with an installing worker (returns without waiting), a lost handover on a controlled page (resolves after the new bound) and an uncontrolled page (still waits for activation). All three fail without the fix.
Dart:
dart analyzeis clean, and 102 tests pass, including integration.Playwright: 17 / 17 in Chromium.
npm run verify: the committed Dart assets match the build output.Repeated deploys of the example app (Chromium 152, local server):
Merging alongside #24
This branch is based on
masterand is independent of #24. Whichever of the two merges second will conflict onCHANGELOG.md(both add an Unreleased section) and on the generatedlib/src/assets/bootstrap_template.dart. The template should be regenerated withnpm run build:allrather than merged by hand. We have the two merged on our fork: Vitest 278, Dart 108 and Playwright 17 all pass, andnpm run verifyis clean. We're happy to rebase this branch once #24 is in.