---
slug: "service-worker-ghost-on-localhost"
title: "When the Ghost of Another App Haunts Your Browser During Local Development"
description: "Stale fragments of another app flickering on localhost? It's likely an old Service Worker. Here's how to detect it and disable SW in SvelteKit dev mode."
url: "https://www.ytyng.com/en/blog/service-worker-ghost-on-localhost"
publish_date: "2026-05-22T10:06:48Z"
created: "2026-05-22T10:06:48.201Z"
updated: "2026-05-22T10:50:34.966Z"
categories: []
keywords: ""
featured_image_url: "https://media.ytyng.com/resize/20260522/78db1728b5544e3693d87f124f71ed7a.png.webp?width=768"
has_video: false
has_music: false
video_urls: []
music_urls: []
lang: "en"
---

# When the Ghost of Another App Haunts Your Browser During Local Development

I usually build apps with SvelteKit, and when I open `http://localhost:5173/` and reload over and over, I sometimes see fragments of a completely different app — one that isn't even running right now — flickering in and out. It makes local development really hard to focus on.

The culprit is almost always a Service Worker (SW) from an older project that's still alive in the `localhost:5173` origin scope.

## Why It Happens

A SW is registered against an origin (scheme + host + port). On a dev machine, multiple apps usually share the same port like `localhost:5173`. So once you register a SW for one app, the next app you serve on the same port has its requests intercepted by that previous SW, which then returns cached responses from the old app.

A SW isn't unregistered by a normal reload, and a hard reload (Cmd + Shift + R) doesn't always remove it either. That's how it becomes a "ghost that refuses to disappear no matter how many times you reload."

## How to Check

Open your browser's DevTools → **Application** tab → **Service Workers**. You'll see a list of SWs running on the current origin. If any of them don't belong to the app you're working on, that's your culprit.

While you're there, also peek at **Application → Storage → Cache Storage** to see what the SW has been caching.

![DevTools Application → Service Workers panel](https://media.ytyng.com/20260522/73a70ac3275143da92fd2b813273812b.png)

## How to Fix

Hit **Unregister** next to the offending SW on the Service Workers page, and the ghost is gone. For full hygiene, clear out any related Cache Storage entries too.

## Auto-Disable the SW in SvelteKit Dev Mode

Unregistering manually every time gets old fast. It's much nicer to simply not register the SW in dev mode. If you also make it auto-strip any SW left over from a previous project, switching between apps becomes painless.

Create `src/lib/registerServiceWorker.ts`:

```typescript
export function registerServiceWorker() {
  if (!('serviceWorker' in navigator)) return;

  // Don't register the SW in dev mode.
  // SWs persist at the origin scope (e.g. localhost:5173), so the cache
  // gets polluted whenever you develop a different Vite app on the same port.
  if (import.meta.env.DEV) {
    // Strip any SW left over from a previous dev session
    navigator.serviceWorker.getRegistrations().then((regs) => {
      regs.forEach((reg) => reg.unregister());
    });
    if (typeof caches !== 'undefined') {
      caches.keys().then((keys) => {
        keys.forEach((key) => caches.delete(key));
      });
    }
    return;
  }

  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .catch((err) => console.error('SW registration failed:', err));
  });
}
```

Call it from `+layout.svelte` or `+page.svelte` inside `onMount`:

```svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { registerServiceWorker } from '$lib/registerServiceWorker';

  onMount(() => {
    registerServiceWorker();
  });
</script>
```

### Key Points

1. **Branch on `import.meta.env.DEV`**: An env variable provided by Vite (the build tool SvelteKit uses internally). It's `true` only during `npm run dev`.
2. **Unregister and clear caches in dev mode**: A bare `return` isn't enough. If you ever switch from a prod build back to dev, the SW is still camped at that origin. Explicitly stripping it out keeps every dev session clean from the moment you switch apps.

## When You Decide to Drop PWA Support

If you change your mind about PWA and just delete the SW file outright, the SW registered in users' browsers stays alive, and the old caches keep getting served forever.

The safe approach is to leave the SW file in place but replace its contents with a stub that unregisters itself. Browsers check for SW updates on every navigation, so the next time the user visits, the stub version activates, and both the cache and the registration get cleaned up automatically.

```js
self.addEventListener('install', () => self.skipWaiting());

self.addEventListener('activate', (event) => {
  event.waitUntil(
    (async () => {
      const keys = await caches.keys();
      await Promise.all(keys.map((key) => caches.delete(key)));
      await self.registration.unregister();
      const clients = await self.clients.matchAll({
        includeUncontrolled: true,
        type: 'window',
      });
      clients.forEach((client) => client.navigate(client.url));
    })()
  );
});
```

Make sure to pass `{ includeUncontrolled: true, type: 'window' }` to `matchAll`. The default arguments only return clients this new SW is already controlling, so tabs still attached to the old SW would be missed otherwise.

## Summary

- If something on the page won't go away after a reload, suspect a Service Worker
- Check DevTools → Application → Service Workers, and Unregister if needed
- Configure your project to skip SW registration in dev mode from the start
- When dropping PWA support, replace the SW with a self-unregistering stub before deleting it
