Description
A client that remains open across a production update can keep the previous Next.js webpack runtime in memory while a newly activated service worker immediately takes control of that page. The next App Router navigation can then combine the old client runtime with a React Server Component response from the new deployment and crash in webpack with Cannot read properties of undefined (reading 'call').
This was observed once in production as DISSCOUNT-FRONTEND-F. The product page is where the incompatible navigation happened, but the evidence does not point to product domain logic.
Production evidence
- Event
807fe7821c7a4d90b9513548b06fec67 occurred on 2026-08-13 at 05:04:35 UTC in Chrome 150 on Windows.
- Sentry recorded one occurrence and no recurrence in the preceding 90-day search.
- The replay breadcrumbs span activity from at least 2026-08-12 at 05:08 UTC through the failure. Product and price requests repeatedly completed with HTTP 200, so the page had been functioning before the crash.
- Immediately before the error, the user submitted a search and the App Router fetched
/products?q=%C5%A1e%C4%87er&_rsc=... with HTTP 200.
- The stack uses webpack runtime
webpack-b54c8363719dcd3f.js and route chunk app/products/page-b3222101cbf5cd19.js.
- Current production HTML uses
webpack-9b085f7ea89462a4.js. The old b54c... runtime now returns HTTP 404.
- The
page-b322... route chunk is still current. Its Last-Modified is 2026-08-07 14:09 UTC, while the current runtime and sw.js were replaced at 19:42 UTC after the later production release. This is consistent with an unchanged route chunk surviving across builds while the build-scoped runtime changed.
- In the current runtime, the failing line is webpack's module loader:
modules[moduleId].call(...). The exception means the old runtime was asked for a module ID that does not exist in its module table.
The exact service-worker lifecycle event was not captured, so the final link is an inference. It is strongly supported by the asset hashes, deployment timestamps, long-lived client, and the current lifecycle configuration.
Root cause
The worker opts into immediate replacement with skipWaiting: true and clientsClaim: true. A new worker can therefore take over a page that still has JavaScript from the previous deployment without reloading that page.
@serwist/next currently registers the worker automatically. reloadOnOnline only adds a reload when the browser returns online. It does not reload a page when an updated worker becomes its controller. There is no controlling or controllerchange handler elsewhere in frontend/src.
Serwist documents this exact risk: long-lived tabs can run site and worker versions simultaneously, and its controlling event identifies when a new worker takes control. See The controlling event and When an unexpected version of the service worker is found.
Likely sequence:
- Build A loads and keeps runtime
b54c... in the page.
- The browser discovers the worker from build B.
skipWaiting activates it immediately and clientsClaim moves the existing page under its control.
- The page is not reloaded, so its build A webpack runtime remains in memory.
- A client navigation requests a fresh build B RSC payload.
- Build A's webpack runtime resolves a build B module ID. The module factory is absent, so webpack throws while reading
.call.
Proposed fix
Add a small client-side PWA update coordinator that reloads exactly once when an updated or external Serwist worker starts controlling an already loaded page.
The safest registration order is to disable automatic registration, attach the controlling listener to window.serwist, then call register(). Ignore initial installation, and reload only when the event represents an update or unexpected external controller. Add a per-navigation or sessionStorage guard so a bad lifecycle edge cannot create a reload loop.
Keep the worker's offline caches and the React Query IndexedDB store intact. A version-alignment reload must not call the identity cache purge or discard queued offline writes.
Done when
Scope and gotchas
- Start in
frontend/next.config.ts, frontend/src/app/sw.ts, and a focused client component under frontend/src/components/custom/pwa/ mounted from frontend/src/app/layout.tsx.
- Do not solve this by filtering the Sentry exception. The crash represents a genuinely broken navigation.
- Do not fold DISSCOUNT-FRONTEND-E into this work. That was a separate one-off Safari failure to load
sw.js and did not show a mixed-build runtime.
- Service workers are disabled in development, so lifecycle verification must use production build artifacts or an isolated test. Do not start another dev server.
Affected area
Frontend (web / PWA)
Description
A client that remains open across a production update can keep the previous Next.js webpack runtime in memory while a newly activated service worker immediately takes control of that page. The next App Router navigation can then combine the old client runtime with a React Server Component response from the new deployment and crash in webpack with
Cannot read properties of undefined (reading 'call').This was observed once in production as DISSCOUNT-FRONTEND-F. The product page is where the incompatible navigation happened, but the evidence does not point to product domain logic.
Production evidence
807fe7821c7a4d90b9513548b06fec67occurred on 2026-08-13 at 05:04:35 UTC in Chrome 150 on Windows./products?q=%C5%A1e%C4%87er&_rsc=...with HTTP 200.webpack-b54c8363719dcd3f.jsand route chunkapp/products/page-b3222101cbf5cd19.js.webpack-9b085f7ea89462a4.js. The oldb54c...runtime now returns HTTP 404.page-b322...route chunk is still current. ItsLast-Modifiedis 2026-08-07 14:09 UTC, while the current runtime andsw.jswere replaced at 19:42 UTC after the later production release. This is consistent with an unchanged route chunk surviving across builds while the build-scoped runtime changed.modules[moduleId].call(...). The exception means the old runtime was asked for a module ID that does not exist in its module table.The exact service-worker lifecycle event was not captured, so the final link is an inference. It is strongly supported by the asset hashes, deployment timestamps, long-lived client, and the current lifecycle configuration.
Root cause
The worker opts into immediate replacement with
skipWaiting: trueandclientsClaim: true. A new worker can therefore take over a page that still has JavaScript from the previous deployment without reloading that page.@serwist/nextcurrently registers the worker automatically.reloadOnOnlineonly adds a reload when the browser returns online. It does not reload a page when an updated worker becomes its controller. There is nocontrollingorcontrollerchangehandler elsewhere infrontend/src.Serwist documents this exact risk: long-lived tabs can run site and worker versions simultaneously, and its
controllingevent identifies when a new worker takes control. See The controlling event and When an unexpected version of the service worker is found.Likely sequence:
b54c...in the page.skipWaitingactivates it immediately andclientsClaimmoves the existing page under its control..call.Proposed fix
Add a small client-side PWA update coordinator that reloads exactly once when an updated or external Serwist worker starts controlling an already loaded page.
The safest registration order is to disable automatic registration, attach the
controllinglistener towindow.serwist, then callregister(). Ignore initial installation, and reload only when the event represents an update or unexpected external controller. Add a per-navigation orsessionStorageguard so a bad lifecycle edge cannot create a reload loop.Keep the worker's offline caches and the React Query IndexedDB store intact. A version-alignment reload must not call the identity cache purge or discard queued offline writes.
Done when
tsc --noEmit, and lint pass, and the production build output confirms the intended registration order.Scope and gotchas
frontend/next.config.ts,frontend/src/app/sw.ts, and a focused client component underfrontend/src/components/custom/pwa/mounted fromfrontend/src/app/layout.tsx.sw.jsand did not show a mixed-build runtime.Affected area
Frontend (web / PWA)