Sometimes the fix is delete. We learned this the hard way when a service worker started eating our Android app alive — four commits, three hours, and a net deletion of 133 lines of code.
The Symptom
Our Reader app is a Capacitor-wrapped Vite + React frontend for reading Postlark blogs on Android. One day it started showing blank screens. Not consistently, which made it worse. First navigation after install? Fine. Second navigation? White void. Cold start after a Play Store update? Blank. The kind of bug that makes you question whether you should have just shipped a WebView pointing at the live site and called it a day.
The First Wrong Fix
The instinct was to treat the symptom. We added an ErrorBoundary around every route, wrote a lazyRetry() wrapper that would re-attempt chunk imports after 500ms if the first load failed, and sprinkled retry buttons across the main views. Defensive coding at its most optimistic.
It helped — sort of. Some cold-start crashes stopped. But the white screen kept reappearing in different shapes, on different devices, at different times. The retry mechanism was papering over something deeper.
Finding the Actual Culprit
VitePWA's service worker.
In a normal browser tab, service workers are genuinely useful. They cache assets, enable offline access, speed up repeat visits. VitePWA makes adding one trivially easy — a few lines in your Vite config and you've got a fully functional PWA. We'd enabled it early on, thinking "free offline support, why not?"
But Capacitor doesn't work like a browser. It serves your web assets from the device's local filesystem. The bundled app already has every file it needs. There is nothing to cache. The worker was solving a non-existent problem.
Worse, it was manufacturing real ones. When a lazy-loaded route was requested, the active worker would intercept the request and attempt to serve its cached version. On first navigation, chunks loaded fine because the worker hadn't fully activated yet. On subsequent navigations, the now-active worker intercepted requests and returned stale or empty responses.
White screen.
The Chicken-and-Egg Spiral
OK, so just unregister the worker on native platforms. Easy, right?
Attempt one: check the platform at app startup, and if it's native Capacitor, unregister any existing worker before rendering. This worked for fresh installs. But Play Store updates introduced a new wrinkle: users who already had the old worker registered would hit the same issue. The old worker intercepts the loading of the very module that contains the unregistration code. Old worker serves old code. Old code doesn't know about the fix.
Chicken, meet egg.
Attempt two: move the unregistration into a raw inline <script> in index.html, before any ES module imports. HTML files are served from the local filesystem, not through the worker, so the cleanup runs first.
Technically correct. But at this point we had error boundaries, retry wrappers, platform detection checks, inline scripts, and a growing pile of version bumps scattered across five files. The codebase was getting more complex to support something that was actively hostile.
Delete Was the Right Answer
Three hours in, we stopped and asked: what exactly are we fighting to preserve?
Service workers in a Capacitor app provide zero benefit. The files are already on the device. Offline mode? It's the default — there is no server to call for static assets. Caching? You're caching files that are already local. The entire value proposition evaporates inside a native wrapper.
The final commit removed vite-plugin-pwa from the build config, stripped out every cleanup script and workaround, and reverted the error boundaries back to plain lazy() imports. Net result: over 130 lines deleted, zero lines of workaround remaining.
The blank screens vanished. No edge cases. No platform checks. Just gone.
You Are Not Alone in This
Search "Capacitor service worker blank screen" and you'll find GitHub discussions and framework-specific threads going back years. The VitePWA issue tracker has reports of workers failing to detect new versions in production Ionic and Capacitor builds. Capacitor's own docs note that workers don't play well with the native bridge on Android. On iOS it's even messier — WKWebView flat-out doesn't support them, so you get platform-inconsistent behavior stacked on top of everything else.
The pattern repeats: developer adds PWA support because it looks like free performance, wraps the app in Capacitor for native distribution, and the two systems fight over who owns the request pipeline. Nobody wins. The user gets a white rectangle.
When to Stop Fixing
"Add VitePWA" is one of those Vite plugin recommendations that circulates without much scrutiny. For a standard web app behind a CDN, it's solid. For anything running inside a native WebView — Capacitor, Cordova, Electron with custom protocols — think hard before enabling it.
The broader point isn't about service workers specifically. It's about the moment when you're three workarounds deep and each one spawns a new edge case. That's the signal to stop patching and start questioning. Does this feature belong here? Is it solving a real problem or a theoretical one?
Our most productive commit that evening was the one that removed code. Sometimes the best engineering is knowing what to subtract.