Repository navigation
fix: keep PWA app shell and its files consistent after a rebuild - #24807
Conversation
Adds a failing integration test for #24800: an installed PWA goes blank after a frontend bundle rebuild because the service worker shipped by the rebuild takes over an already-open page mid-session (skipWaiting + clientsClaim) and then 404s the old-hash bundle the loaded page still references. A faithful reproduction needs two frontend builds with different bundle hashes, which a single-build IT cannot produce, so the test drives a model service worker (swrebuild/sw-model.js) that mirrors the policy of the generated sw.ts. Whether a freshly installed worker takes over mid-session is read from the real generated sw.js, so the test turns green automatically once the platform stops taking over mid-session. The assertion is made on the still-open page (no navigation), so the result is deterministic. The test currently fails, demonstrating the bug.
A service worker generated by a new frontend build called self.skipWaiting() and therefore activated while pages from the previous build were still open. It took those pages over mid-session and then 404'd the old-hash bundle they still reference (the previous build's files are gone from both the new precache and the redeployed server), leaving the installed PWA on a permanent blank screen. Removing self.skipWaiting() lets the new worker stay in "waiting" until the previous build's pages close, so control is handed over cleanly between page loads instead of mid-load. clientsClaim() is kept so the first page load is still controlled without a reload (offline-on-first-load). Also corrects the reproduction IT to detect the takeover policy via the self.skipWaiting() call rather than the bare "skipWaiting" token, which also appears in a workbox doc comment in the bundled service worker. Fixes #24800
|
skipWaiting has been there since #4334. It's not 100% clear to me what the impact of this is |
|
|
Maybe @vursen has some knowledge about this? The test here seems to be worthless and automated testing of what happens when you rebuild the service worker is difficult |
Unfortunately, I don't know much about this |
|
The issue remains with The sequence I use is:
The only way to recover from this has been to delete the PWA from the iOS Home Screen and re-add it. With the "fix" in place, "killing" the PWA and restarting it now works, so that's a little better. Here is the server console log of the redeployment: Here is the PWA console log after the redeployment but before tapping anything: (I normally don't wait long enough here to get this 404—but that's immaterial.) Here is the PWA console log after tapping something in the app: — According to Claude Code: The blank screen has two independent paths, and PR #24807 only fixes one of them:
The real fix needs to change the Before ( After ( So the PR narrows the recovery from "uninstall + reinstall" to "kill + relaunch," which is a meaningful improvement. The remaining issue — blank on the first tap after redeploy without killing first — requires a separate fix: the So, the PR is a partial fix, it changes the recovery UX, but there's a second defect in the |
In development mode the service worker sent every /VAADIN/ request network-first, so a precached app shell from the previous build loaded its entry script from the server, which no longer had it, and the page stayed blank. Precached dev bundle files are now served from the precache. The dev server update of an edited sw.ts now asks the rebuilt worker to skip waiting, as the generated worker no longer does that by itself. Removes the model-based IT, which did not exercise the real service worker.
|
Pushed 1095b5e on top of this PR, which addresses the remaining blank page reported above. (It replaces #26464, now closed.) Root cause of the remaining blank page: in development mode (which includes dev-bundle builds), What the new commit does:
Verified manually with a real Trade-off: Workbox only cleans outdated precache entries on activation, so builds deployed while the app stays open accumulate in the precache until the newest worker activates. @joelpop could you retry your iOS sequence with this branch? The first tap after a redeploy should no longer give a blank page without killing the app first. |
A new build's worker now waits for a message instead of activating itselfsequenceDiagram
participant Vite as vite-devmode.ts hot.on("sw-js-update")
participant Reg as ServiceWorkerRegistration
participant Worker as New worker (sw.ts)
Vite->>Reg: registration.update()
Reg->>Worker: installs new build
Worker-->>Worker: reaches state "installed", stays waiting (no skipWaiting call)
Note over Worker: previously self.skipWaiting() ran here unconditionally
Vite->>Worker: activateWaitingServiceWorker() watches statechange (new)
Vite->>Worker: postMessage Vaadin.ServiceWorker.skipWaiting (new)
Worker->>Worker: message handler calls self.skipWaiting() (new)
Worker->>Worker: activates, clientsClaim(), drops old precache entries
Note over Worker: without this message the worker stays "waiting" until every open window closes
Diagram Bot draws the mechanism this pull request touches; it does not review the change. Verify it against the diff.
|
vaadin-review-bot
left a comment
There was a problem hiding this comment.
Reviewed the changes — left 1 comment.
| Finding | |
|---|---|
| 🧪 | Neither changed behavior is reachable from any existing test module, so a regression would go unnoticed. |
| // Precached files of a dev bundle are left to the precache route: after a | ||
| // rebuild the server no longer has them, but the app shell served from the | ||
| // precache still references them. | ||
| registerRoute( |
There was a problem hiding this comment.
🧪 Neither changed behavior is reachable from any existing test module, so a regression would go unnoticed.
All PWA IT modules (flow-tests/test-pwa, flow-tests/test-frontend/vite-pwa*) inherit vaadin.frontend.hotdeploy=true from flow-tests/pom.xml. In hotdeploy mode the service-worker plugin never runs injectManifestToSWPlugin, so self.__WB_MANIFEST stays undefined and manifestEntries holds only the runtime entries (icons, manifest, offline page) — none under /VAADIN/. The new !isManifestEntryURL(url) term is therefore always true there, and the dev-bundle mode the fix targets has no PWA test module at all.
The same applies to the lifecycle change: nothing asserts that a new worker waits, or that the Vaadin.ServiceWorker.skipWaiting message activates it. Reverting either hunk would keep CI green.
sw.ts:201 · tests
There was a problem hiding this comment.
Added coverage in a106210 and the follow-up commit. There is now a dev-bundle variant of the PWA test module (flow-tests/test-pwa/pom-devbundle.xml, hotdeploy=false), so the bundle files really are in the precache manifest. ServiceWorkerUpdateIT runs against the real generated service worker:
precachedBundleFile_removedFromServer_servedFromPrecache: a test filter answers/VAADIN/build/*with 404 while a cookie is set, simulating the server after a rebuild. The precachedindexhtml-*.jsmust still come back 200 through the worker.newServiceWorker_waitsUntilSkipWaitingRequested: registers the worker script under another URL to install a new worker. The new worker must stay inwaitingwhile the page is open, then activate and take control once it receivesVaadin.ServiceWorker.skipWaiting.
Against the previous sw.ts, both fail (404 instead of 200, and the new worker activates right away). The precache test runs only in the dev-bundle module, which sets vaadin.test.devBundle for failsafe. There it fails, rather than skipping, if no bundle script is precached. With the Vite dev server nothing is precached, and in production the files are served from the precache anyway. The waiting test passes in all three variants. Still not covered: the vite-devmode.ts side that sends the message after a hot sw.ts edit.
Adds a dev bundle variant of the PWA test module, where the bundle files are precached, and tests that a precached bundle file the server no longer has is still served from the precache, and that a new service worker waits until it is asked to skip waiting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The precache test now runs only in the dev bundle module, where it fails instead of skipping when the bundle scripts are not precached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Vite dev server proxy answers sw.js with a query string with 404, so a new worker cannot be installed that way in the hot deploy module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Production service workers no longer activate a waiting worker on request, so a new build's worker always waits until the previous build's pages are closed. The dev server still uses the message to apply an edited sw.ts to the open page.
|
caalador
left a comment
There was a problem hiding this comment.
Looks ok, but would be good to have it tested on iOS with the latest changes.
Just to note: the first update after upgrading will still hit the bug. Users' devices run the previous service worker, which still calls skipWaiting() and lacks the route fix. The fix takes effect only for rebuilds after the fixed version has been deployed once.
📦 Snapshot published
Built without running tests. Look at the checks on this pull request before relying on it. Every new commit on this branch republishes it while the How to use it<repository>
<id>vaadin-snapshots</id>
<url>https://maven.vaadin.com/vaadin-prereleases</url>
<snapshots><enabled>true</enabled></snapshots>
</repository>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>flow-bom</artifactId>
<version>25.4.pwa-after-rebuild-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- After flow-bom, so that its Flow version wins -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>25.4-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Replace the application's own |
|
I tried to first reproduce the original problem with 25.3.2 but cannot, so it is difficult to test the change. What I tried was
Is there something I am missing? |
|
@Artur- What you see is the normal online path, and that one already worked before this PR. The click gets The blank screen only shows up when the reload is answered by the old worker's offline fallback: the I could only reproduce it by forcing that branch: I made the navigation fetch fail while the server served
To check the fix itself without the timing, in a dev-bundle app with this branch: install the worker, rebuild with a frontend change, restart the server, then in the still-open page set the network to offline only for navigations (or fake |
|
By setting a breakpoint in the TaskRunDevBundleBuild constructor and reloading when the server hits the breakpoint, I can see a problem in desktop Chrome - seems like the same. This goes away with this PR - not sure if it is actually the same thing |
|
@Artur- Yes, that's the same bug. The server's answer is just different.
So your breakpoint setup is a reliable desktop way to reproduce it, without needing iOS timing. I've added it to the test summary in the PR description. |
Explain that a new service worker waits until every open window and tab is closed before it activates, so a single reload doesn't pick up a new build, on the PWA configuration page. Documents vaadin/flow#24807 (`413503abaa8506b9a163b42c0d78bec777f4724e`).
|
Pull request created: #6619
|
|
Documentation Bot: Draft documentation pull request for this change: vaadin/docs#6619 Files updated:
It was written from the state of this pull request as you see it now. Please review it and mark it ready for review.
|



Review risk: two-way door — only the generated PWA service worker (
sw.ts) and the dev server's service worker update change; affects apps using@PWAwith offline support, reverting is a one-file change.Fixes #24800
Summary
After a frontend rebuild, an installed PWA could show a permanent blank page on the next reload. This happened when the service worker answered the reload with its precached app shell from the previous build. In development mode, the worker fetched every
/VAADIN/file network-first, so the old shell's entry script was requested from the server, which no longer had it. The worker then passed the server's 404 through and never checked the precache.Removing only
skipWaiting()was not enough: the blank page still appeared, and a reload no longer recovered it because the new worker stayed in "waiting".What changed
/VAADIN/files that are in the precache are now served from the precache instead of network-first. The files have a content hash in their name, so this is the same cache-first handling production builds already use. Non-precached/VAADIN/URLs (e.g. the Vite dev server's modules) are still network-first.self.skipWaiting(). A worker from a new build now waits until the previous build's pages are closed. Activating right away removed the previous build's files from the precache while those pages could still load from it (e.g. lazy route chunks in another open window).clientsClaim()is kept so the first install still controls the page without a reload.sw.tsstill takes effect in the open page. After the update check, the dev server asks the rebuilt worker to skip waiting through a newVaadin.ServiceWorker.skipWaitingmessage. The worker handles that message only in development builds (including dev bundles); a production worker ignores it.Update behavior (changed default)
How a new service worker reaches users changes with this PR, and this should be mentioned in the release notes and the PWA docs:
index.htmland files on reload. Offline, or when the old worker serves the precached app shell, they get the previous build until the app is closed.Vaadin.ServiceWorker.skipWaitingmessage is handled only in development builds. Whether Flow should provide a supported "new version available, reload" mechanism is left as a follow-up decision.Test summary
@PWAapp in dev-bundle mode, driven by headless WebKit (Playwright) and Chromium: install the worker, rebuild with a frontend change and restart the server, then click in the still-open page.maingives a 404 on the oldindexhtml-*.jsand a blank page. With this change, the app loads.mainthe old shell's bundle files fail withno-responsefrom theNetworkFirstroute and the page stays blank. With this change they come from the precache and the app loads.sw.ts, in Chromium: the rebuilt worker activates and takes over the open page. Without the message it stays in "waiting".Vaadin.ServiceWorker.skipWaitingmessage; in production it keeps waiting after the message.sw.ts. The precache test runs only with a dev bundle, and fails there if the bundle scripts are not precached. The waiting test also runs in production. It is skipped with the Vite dev server, which does not servesw.jswith a query string.sw.ts.