> ## 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.

# Extension performance playbook

> Keep extension startup fast and runtime overhead low across content scripts, service workers, and UI surfaces. Prevent common performance regressions.

Performance regressions in your extension come from a short list of places. The most common are service worker cold-starts (initial startup delays), heavy content-script injection, oversized UI bundles, and media-heavy web-accessible resources.

The checklists below map each type to a concrete fix.

## Performance optimization capabilities

| Area | What this helps you optimize |
| - | - |
| Content scripts | Injection cost, DOM work timing, and remount overhead |
| Service worker | Startup path, event handling cost, and cold-start behavior |
| UI surfaces | Initial render size and optional feature loading |
| Assets | Bundle size, icon/media weight, and web-accessible resource footprint |
| Operations | Continuous integration (CI) checks and runtime regression tracking |

## Fast optimization pass

1. Measure first-run behavior on one target browser.
2. Trim synchronous startup work in content scripts and background handlers.
3. Defer optional UI features until after initial render.
4. Re-check build output size and smoke-test critical flows.

## Content scripts

* Keep entry files small and defer non-critical work.
* Avoid heavy synchronous DOM scans at `document_start`.
* Scope observers and event listeners; clean up on remount/dispose.
* Split reusable logic into shared modules, but avoid fragile dynamic import patterns.

## Background/service worker

* Minimize work at startup; initialize lazily when events arrive.
* Cache stable computed state where safe.
* Avoid long-running synchronous tasks in event handlers.
* Watch service worker dependency churn during active development.

## UI surfaces (popup/options/new tab/sidebar)

* Keep initial render path lightweight.
* Load large optional UI features on demand.
* Use framework dev tools only when needed in local debugging sessions.
* Keep styles modular and avoid large global CSS payloads.

## Assets and resources

* Optimize icon/image sizes by target use.
* Limit web-accessible resources (WAR) exposure to required assets.
* Keep public assets intentional; remove stale files.
* Verify generated `dist/<browser>` output size regularly.

## Trim what ships

Every byte in `dist/<browser>` is a byte that each user downloads on install and on every update, and a content script's assets travel with it to every matched page. Here is what `extension build` already leaves out, where the remaining weight comes from, and where to read the number.

**What the build excludes.** Output starts from `manifest.json` and follows references. It ships the entries that the manifest names (background, content scripts, pages, icons, locales), the modules and assets that those entries import, and everything under `public/`. A file in `src/` that nothing references is not emitted. A notes file, an unused module, a fixture folder, or a stray image next to your icons never reaches `dist/`. You do not need an ignore list for them.

**`public/` versus imported assets.** The two paths behave differently, and the difference is the usual source of surplus bytes:

* `public/` is copied to the output root as-is, folder structure included, and nothing is checked against it. A stale file there ships. Treat the folder as a list of files that you have decided to ship, and prune it.
* An asset imported from JavaScript lands at `assets/<name>.<hash>.<ext>` and ships once, only when the import exists.
* An asset referenced from an HTML page is written twice: under `assets/<relative path>` and at its source path, so a reference from script code keeps working. A file referenced from both an HTML page and a JavaScript import ships three times. Import a shared image from JavaScript, or move it to `public/` and reference it by root path, so it is written once.

**`web_accessible_resources` scope.** On Manifest V3 the build adds two kinds of file to `web_accessible_resources`, under the union of your content scripts' `matches`: every chunk that a content script loads with `import()`, and every non-script file under `assets/`, whichever page or script put it there. Files copied from `public/` are not added. Two consequences follow. Keep `matches` as narrow as the feature allows. Keep page-only images out of `assets/` (reference them from `public/` by root path), so a content script's `matches` does not expose them. See [Make an extension file readable by a web page](/docs/implementation-guide/web-accessible-resources).

**Read the size.** `extension build` prints a tree of the output with the size of each file and closes with the total:

```plaintext theme={null}
⏵⏵⏵ Extension built for production in dist/chromium (171.6 KB).
```

With `--zip`, a second line names the archive and its compressed size. The archive holds the contents of `dist/<browser>` and nothing else, under `dist/<name>-<version>-<browser>.zip`, where the name is the manifest `name` lowercased with punctuation removed:

```plaintext theme={null}
⏵⏵⏵ Packaged dist/trimdemo-1.0.0-chromium.zip (142.9 KB).
```

For a script, `extension build --output json` returns `total_bytes`, `largest_asset_bytes`, and the size of each entry in `zip_artifacts`, and the same summary is written to `dist/extension-js/<browser>/build-summary.json`. Compare `total_bytes` across commits in CI to catch a size regression before a store review does. See [`build`](/docs/commands/build).

## Performance budgets

Production builds (`build`) emit a **warning** when a bundle exceeds its per-category size budget. Budgets are warn-only (they never fail the build) and are enabled in production mode by default.

| Category | Default budget | Why |
| - | - | - |
| Content scripts | 512 KiB | Injected on every matched page navigation. |
| Service worker | 512 KiB | Wakes from cold on each event; large bundles slow startup. |
| Pages / UI (popup, options, sidebar, devtools, new tab) | 1 MiB | Opened on demand. |
| Shared chunk | 512 KiB | Loaded by every page that imports it. |
| Public (code files copied from `public/`) | 1 MiB | Shipped as authored, so the fix is at the source file. |
| Runtime (output-root assets and every `.wasm` file) | 1 MiB | Hashed wasm cores and their sibling helpers. |

Override any category in [`extension.config.js`](/docs/features/extension-configuration) via `perfBudgets` (values in bytes):

```js theme={null}
export default {
  perfBudgets: {
    "content-script": 768 * 1024,
    "service-worker": 512 * 1024,
    page: 2 * 1024 * 1024,
    shared: 512 * 1024,
    public: 2 * 1024 * 1024,
    runtime: 8 * 1024 * 1024,
  },
};
```

## Operational checks

* Run multi-browser build checks in CI.
* Track regression signals in end-to-end (E2E) runtime duration over time.
* Add smoke tests for critical extension flows (install, open UI, content script activation).

## Common performance pitfalls

* Large content-script bundles loaded on every matched page
* Heavy work at `document_start` without guard conditions
* Service worker handlers doing synchronous, non-essential initialization
* UI surfaces shipping large global CSS/JS when you only use partial features

## Next steps

* Review [Playwright E2E](/docs/workflows/playwright-e2e).
* Review [Security checklist](/docs/workflows/security-checklist).


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