> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Page reload and hot module replacement (HMR)

> Understand how Extension.js chooses the fastest safe update path during development, from HMR for styles to hard reloads and restart-required changes.

Keep development fast by using the lightest update strategy for each file change.

Need fast feedback after each file change? Extension.js chooses the safest and fastest update path automatically:

* **HMR (hot module replacement)** when module updates are safe
* **Classified reloads** (full, service worker, content scripts, or notify-only page) when runtime assets change
* **Restart required errors/warnings** when entrypoint structure changes

## Reload prerequisites (devtools setup dialog)

Before evaluating reload behavior, confirm the same setup checks shown in the Extension.js devtools **Confirm setup** dialog:

| Setup step | Why it matters for reload/HMR |
| - | - |
| Enable Developer mode in `chrome://extensions` | Ensures that extension runtime updates (including service worker changes) apply reliably during development. |
| Accept the local network access prompt for content scripts | Content-script reload workflows need this when the browser requests network permission. |

If you skip either step, reload behavior can appear inconsistent even when the build pipeline is healthy.

## Reload tiers

### 1) Hot module replacement (fastest path)

Extension.js uses HMR when code can update without restarting the extension's background processes and event listeners.

Common examples:

* Scripts attached to extension HTML pages (accept updates through injected HMR wrappers)
* Background script modules in non-service-worker flows
* Scripts registered via the [`userScripts` API](https://developer.chrome.com/docs/extensions/reference/api/userScripts)
* CSS updates in content-script/runtime wrappers where supported

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/qjXVWBrLq74?rel=0" title="Extension.js: edit a line, save, the page updates in place" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>

### 2) Classified reloads

When a change needs more than HMR, Extension.js classifies it into one of four reload kinds:

| Kind | What happens |
| - | - |
| `full` | The whole extension reloads, so manifest and assets are re-read from disk. |
| `service-worker` | The background service worker restarts. |
| `content-scripts` | The changed content-script entries re-inject into their tabs. |
| `page` | Notify only. Livereload refreshes the page surface, no extension reload runs. |

The decision comes from the compiler's chunk graph, not from filenames, and follows a fixed order:

1. **Forced full.** A change to `manifest.json` or anything under `_locales/` always classifies as `full`, no matter what else changed.
2. **Chunk membership.** Extension.js looks up which chunks contain each changed source. A source in a `background/` chunk means `service-worker`. A source in a `content_scripts/` chunk means `content-scripts`.
3. **Changed static asset.** Some changed files exist in the output directory but belong to no chunk: an icon, a web-accessible resource, a DNR ruleset. These force `full`, so the browser re-reads them from disk.
4. **Name heuristics.** Only for sources the chunk graph does not know: paths matching `background` or `service worker` patterns classify as `service-worker`.
5. **Content fallback.** If the manifest declares content scripts, remaining unknown changes re-inject every content-script entry.
6. **Notify-only page.** Everything else is a `page` instruction: livereload owns the refresh, and the reload announcement still fires.

A source that lives in both a service-worker chunk and a content-script chunk fans out to both paths. One save triggers the SW restart and carries the stale content-script entries for re-injection, in a single classified instruction.

Each dev reload is announced with a single context label built by the dev server. The format is `context (fileA, fileB +2 more)`, for example `service_worker + content_script (shared/api.ts)`. The same label appears verbatim in CLI stdout, in the page devtools console, and in the devtools pill, so you can correlate a reload across all three without guessing.

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/q7BRUgMB7O4?rel=0" title="Extension.js: one save to a shared module, worker and content script reload" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>

If the browser serves a stale cached service worker after a reload, the dev server detects the version mismatch and resynchronizes the extension automatically. You should not need to manually remove and re-add the extension to recover.

> **Why do content script filenames include hashes in dev?**
>
> Chrome aggressively caches `chrome-extension://` resources. A stable filename like `content-0.js` can serve stale code even after a full extension reload. Extension.js appends a short build hash in development (for example, `content-0.abcd1234.js`). Each rebuild produces a fresh URL that bypasses the cache. Production builds use clean names. Set `hashContentScripts: false` under `commands.dev` in `extension.config.*` to opt out and keep stable dev filenames. A top-level key of the same name is ignored.

### 3) Restart required (dev server)

When extension entrypoint references change (not just module contents), Extension.js reports restart-required diagnostics. This prevents you from continuing with a stale dependency graph.

Typical restart-required scenarios:

* Manifest script entrypoint list changes
* HTML entrypoint script/style references change
* `pages/` / `scripts/` file set changes in watch mode (especially removals)

## Behavior matrix

| Change type | Classified result |
| - | - |
| JS/CSS module updates in existing entries | HMR where supported |
| `manifest.json` or `_locales/**` edits | `full` (forced, regardless of other files) |
| A source in a background chunk | `service-worker` |
| A source in a content-script chunk | `content-scripts` (only the affected entries) |
| A source in both background and content chunks | `service-worker` + content re-inject (fan-out) |
| A changed emitted static asset (icon, web-accessible resource) | `full` |
| A page-only source (popup, options, sidebar, devtools) | `page` (notify only, livereload refreshes) |
| Entrypoint structure updates (manifest lists, HTML script/style refs) | Restart required |
| `pages/` or `scripts/` file add/remove during watch | Warning/error with restart guidance |

## Safari

The tiers above are the Chromium and Gecko reload paths. From 4.1.20, a Safari dev session updates by a different route. Each save rebuilds `dist/safari`, resyncs the app's resources with an incremental `xcodebuild`, and reloads the extension in Safari. Measured on Safari 26.5.2, that is roughly 4 seconds from save to reloaded extension, with no HMR tier. See [Building Safari extensions](/docs/browsers/safari).

## How the dev runtime gets injected

The reload plugin instruments your build in five injection steps, then prunes:

1. Strip the dev-server runtime from content-script chunks, which cannot host it.
2. Set up the reload strategy on the background and content-script entries.
3. Inject the service-worker scripts-replay shim, so `/scripts/*` injections re-run on edit.
4. Inject the bridge producer, so the background forwards console output to the control channel.
5. Inject the bridge relay, so content-script console output reaches the same channel.

Afterward, a pruning step manages `hot/`. Hot-update chunks are fetched from disk on the extension origin, so stale generations would accumulate in what ships. Extension.js keeps the current generation plus one previous generation for in-flight fetches, and deletes the rest after every compile.

The whole pipeline is development-only. It is a no-op in production builds and when you pass `--no-reload`.

## Reloading scripts and HTML outside the manifest

`pages/` and `scripts/` follow the same reload strategy as manifest-declared assets.

* Existing module updates can use hot-update paths.
* Entrypoint set changes (add/remove or reference graph changes) may require restart.

**Example:**

```plaintext theme={null}
pages/
└── extra-page.html
scripts/
└── extra-script.js
```

<Note>
  Extension.js recognizes the `/pages` and `/scripts` folders for hot-reloading
  and treats each entry as a separate page or script it can reload
  independently.
</Note>

<Note>
  Scripts you inject programmatically with `chrome.scripting.executeScript` from
  `/scripts/*` are **replayed** on edit. When you change a file under
  `/scripts/`, Extension.js re-runs the same injection on the same tab and tears
  down the previous mount, so dynamically injected scripts update live, the way
  declarative `content_scripts` do, instead of leaving stale DOM until you
  manually re-trigger the call. This is a dev-only convenience.
</Note>

## Best practices

* Keep entrypoint references stable during active dev sessions to maximize HMR.
* Batch `manifest.json` and locale edits to avoid repeated hard extension reloads.
* Use `pages/` and `scripts/` for off-manifest assets, then restart when file sets or entry wiring changes.
* Treat restart required diagnostics as intentional safety checks, not transient warnings.

## Next steps

* Review off-manifest asset flows in [Special folders](/docs/features/special-folders).
* Understand structural update outcomes in [Dev update behavior](/docs/workflows/dev-update-behavior).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.