Manifest V3: What Changed and How We Adapted onHover

Sourabh R.

Founder

10 min read

If you built a Chrome extension before 2023, you know the migration story: everything broke at once, the docs were thin, and the community was angry. The Chrome Manifest V3 migration guide most developers needed didn't exist yet. We went through it building onHover — here's what actually changed, what it cost us, and what the architecture looks like now.

What Manifest V3 actually changed

Background pages replaced by service workers

MV2 extensions ran a persistent background page — a long-lived HTML context that stayed alive the entire time Chrome was open. MV3 replaced it with a service worker: ephemeral, event-driven, and shut down after a few seconds of inactivity.

Any code that assumed persistent in-memory state broke immediately after the migration. Global variables in the background script no longer survive between events. If the service worker sleeps and wakes up, it starts completely fresh — with no memory of what it was doing before.

MV2 pattern (broken in MV3)

Storing state in a global variable and reading it later assumes the service worker is still alive — it won't be.

let activeTabId = null; // Gone after SW sleeps

MV3 service worker solution

Use chrome.storage.session for tab-scoped ephemeral state or chrome.storage.local for anything that needs to persist. Always read from storage — never assume something is in memory from a previous event.

webRequest replaced by declarativeNetRequest

MV2 let extensions intercept, inspect, and modify network requests dynamically via JavaScript. MV3 replaced this with declarativeNetRequest — static JSON rules declared at install time, evaluated by the browser, not your code.

For onHover, network inspection moved to the debugger API — attaching to Chrome's DevTools protocol to observe network events and apply throttling. More powerful in some ways, more restricted in others.

Tighter Content Security Policy

MV3 banned every piece of remotely hosted code from running inside an extension. Everything must now be bundled in the extension package — no CDN-loaded scripts, no eval(), no new Function(). That constraint pushed us toward a proper Vite build pipeline for the first time.

How we adapted onHover for Chrome extension development 2026

  • State — migrated all background state to chrome.storage.session
  • Network throttling — switched from webRequest to chrome.debugger.attach + Network.emulateNetworkConditions
  • Script injection — replaced dynamic eval with chrome.scripting.executeScript using bundled functions
  • Offscreen documents — used the new offscreen API for DOM-context operations like canvas stitching for full-page screenshots
  • Build pipeline — added Vite with viteStaticCopy to compile content scripts and copy module files to the dist directory properly

Was the MV3 migration worth it?

The migration took about 3 weeks of focused engineering work. The result is more predictable — service workers with explicit lifecycles are easier to reason about than background pages, which could silently crash and leave the extension in a broken state without warning.

The declarativeNetRequest constraint is the biggest loss of flexibility, but the debugger API covers everything onHover needs. If you're starting a new Chrome extension today, begin with MV3. The ecosystem has caught up since the rocky initial rollout.

The offscreen document API: a practical guide

One of the real pain points in early MV3 development was this: service workers have no DOM access. No document, no canvas, no DOMParser, no Web APIs that depend on a browser context. For a developer toolkit like onHover that does a lot of DOM-heavy work — including full-page screenshots — this was a serious constraint.

The solution MV3 introduced is the offscreen document API. It's exactly what it sounds like: a hidden HTML page that the service worker can create, send messages to, and close when it's done. The offscreen document has full DOM access. It can run canvas operations, parse HTML, use APIs that require a browsing context. The service worker acts as the coordinator; the offscreen document does the work.

Here's how onHover's full-page screenshot feature uses it. The service worker captures viewport segments using the chrome.tabs.captureVisibleTab API, which returns individual viewport screenshots as data URLs. Those get passed to the offscreen document via chrome.runtime.sendMessage. The offscreen document draws each segment onto a canvas in the correct position, then returns the final stitched image as a single data URL back to the service worker. Clean separation of concerns. The service worker orchestrates; the offscreen document renders.

Without offscreen documents, canvas operations would have to happen in content scripts — which run in the page context, have different security constraints, and can interfere with the page's own JavaScript — or in the extension popup, which disappears the moment the user clicks anywhere else. The offscreen API gave us a dedicated, stable DOM context for heavy operations. If you're building a Chrome extension that needs any kind of DOM-based processing in the background, this is the API to learn first.

Managing extension permissions in MV3

Permissions in MV2 were mostly binary: you either had them at install time or you didn't. MV3 introduced a more granular model — optional permissions that you request at the moment a feature actually needs them, rather than front-loading everything into the install prompt.

This matters more than it sounds. The Chrome Web Store install dialog showing a list of powerful permissions is one of the biggest conversion killers for extensions. Users who read permission dialogs carefully will decline to install if the list looks scary. And the Chrome Web Store itself scrutinizes permission requests more closely now — requesting permissions you don't immediately need can delay or block review.

For onHover, we split the permission surface deliberately. Core features — CSS inspection, element highlighting, the image outliner, the color picker — use host permissions declared in the manifest because they need access to page content on every activation. Features that touch more sensitive APIs request those permissions only when the user first enables that specific feature. The debugger permission for network throttling. The tabCapture permission for screenshots. A user who only ever uses the CSS inspection tools in our developer toolkit never sees a debugger permission prompt. They don't need to know that capability exists until they try to use it.

Permission timing strategy

Request optional permissions at the exact moment the user triggers the feature that needs them. A permission prompt that appears in context — right when you click "Enable network throttling" — feels logical. The same prompt appearing at install for a feature you haven't tried yet feels intrusive.

The practical result: a smaller apparent permission surface at install time, better trust with users who read permission dialogs carefully, and a cleaner review experience with the Chrome Web Store. If you're designing a new Chrome extension in 2026, plan your permission split from the beginning rather than retrofitting it later. It's much easier to design around optional permissions than to restructure an extension that already assumes it has everything it needs.

Testing Chrome extensions under MV3 constraints

The development workflow for MV3 extensions is genuinely different from MV2, and it took us a while to build the right habits. The biggest mental shift: you have two separate DevTools contexts now, and you need to be comfortable switching between them.

Service workers don't open DevTools automatically. To inspect your extension's service worker, go to chrome://extensions, find your extension, and click the "service worker" link next to the extension entry. That opens a separate DevTools window for the service worker context — separate console, separate network tab, separate Sources panel. Your content script logs go to the page's DevTools. Your service worker logs go to this other window. Getting comfortable switching between them is a new muscle to develop.

For debugging service worker lifecycle specifically, chrome://serviceworker-internals shows all registered service workers and their current state. You can see whether your service worker is running, stopped, or in a starting state. More importantly, you can stop it manually to simulate the sleep/wake cycle that happens in production.

That manual stop test is the most important habit we developed for MV3 testing. In production, Chrome will shut down your service worker after a few seconds of inactivity. Any state that doesn't survive a service worker restart is a bug waiting to surface for real users — usually at the worst possible moment. Test it explicitly: use the extension for a few seconds, click the Stop button in chrome://extensions, then resume. If anything breaks, you just found a state management bug that you wouldn't have caught in normal development testing where the service worker stays alive continuously.

The other testing habit worth building: load your extension in a fresh Chrome profile with no other extensions installed. Other extensions can interfere with content scripts, inject conflicting CSS, or modify page behavior in ways that make bugs look like your code is broken when the actual cause is an interaction with another extension. A clean profile removes that variable.

What MV3 means for extension users and developers

Step back from the migration pain and look at what MV3 actually achieved. For end users, Chrome extensions built on MV3 are meaningfully more secure. No arbitrary code execution from remote servers means a compromised CDN can't push malicious code into an extension that's already installed on millions of browsers. Static network rules are auditable — you can read exactly what an extension is allowed to block or modify before you install it. Shorter-lived background processes use less memory and battery when you're not actively using an extension.

For developers, the tradeoffs are real and worth acknowledging honestly. State management is more complex. Some APIs that were trivial in MV2 require significantly more code in MV3. The offscreen document pattern for DOM operations adds an extra layer of message passing. There's a steeper learning curve for operations that used to be two lines. That's the actual cost, and the complaints from the developer community during the transition were legitimate.

But the transition period is largely behind us now. The APIs have stabilized. Chrome's documentation has improved significantly. The Chrome Extension Samples repository on GitHub has MV3 examples for most common patterns. Community resources like the chrome-extensions-samples repo and the dedicated Chrome Extensions developer forum have built up a solid body of working MV3 code to reference.

If you're starting a new Chrome extension today, MV3 is the only sane starting point. The old MV2 support window has closed. The ecosystem has caught up. The patterns are documented. Build on MV3 from day one — not because the migration is fun, but because the alternative is building on a foundation that no longer exists.

Frequently asked questions

What is Manifest V3 in Chrome extensions?
Manifest V3 (MV3) is the third version of Chrome's extension platform specification, introduced in Chrome 88 and required for all new Chrome Web Store submissions since 2022. It replaces Manifest V2 with a new architecture: background pages become service workers (which terminate when idle), the blocking webRequest API is replaced by declarativeNetRequest, and the content security policy is stricter. The changes aim to improve performance, privacy, and security.
What is the main difference between Manifest V2 and Manifest V3?
The biggest change is the background page: MV2 used persistent background pages that ran constantly. MV3 uses service workers that activate on demand and terminate after a period of inactivity. This means extensions can no longer maintain in-memory state across arbitrary time periods — state that needs to persist must use chrome.storage. The blocking webRequest API (used by ad blockers) was also restricted, requiring migration to declarativeNetRequest for most use cases.
Are Manifest V2 extensions still supported in Chrome?
Chrome stopped accepting new MV2 extensions on the Chrome Web Store in 2022 for enterprise users and 2024 for the public. Existing MV2 extensions in personal browsers still work as of mid-2026, but Google has stated they intend to phase out MV2 support. New extensions must use MV3. If you're maintaining an older extension, migration to MV3 is necessary for continued Chrome Web Store distribution.
What is a service worker in a Chrome extension?
In MV3, the service worker replaces the background page as the extension's background context. Like web service workers, it runs independently of any browser window, activates in response to events (messages, alarms, browser actions), and terminates after a period of inactivity to free memory. Unlike web service workers, it has access to all Chrome extension APIs. The key constraint: you cannot maintain long-running in-memory state — use chrome.storage for persistence.
What is declarativeNetRequest in Manifest V3?
declarativeNetRequest is the MV3 replacement for the blocking webRequest API. Instead of a JavaScript handler that intercepts and modifies requests in real time, it uses a static rule set declared in the extension manifest or loaded via updateDynamicRules(). This approach is more performant and privacy-preserving but less flexible — you can block, redirect, and modify headers, but you cannot read request bodies or implement complex conditional logic like MV2's blocking webRequest allowed.
How do I migrate my Chrome extension from MV2 to MV3?
The main steps are: (1) Replace background.js persistent page with a service worker. (2) Move any state that was stored in background page variables to chrome.storage. (3) Replace blocking webRequest listeners with declarativeNetRequest rules where possible. (4) Update the manifest to use action instead of browser_action/page_action. (5) Update the content_security_policy format. (6) Test the service worker lifecycle — it terminates unexpectedly, so test that messages and events are handled after wakeup.
Why does my Chrome extension service worker keep stopping?
Chrome terminates extension service workers after approximately 30 seconds of inactivity to reclaim memory. This is by design in MV3. If your extension needs to persist state between service worker activations, use chrome.storage.local or chrome.storage.session. If you need the service worker to run continuously, you need to trigger it with periodic alarms (chrome.alarms) — but keep in mind Chrome also limits how frequently alarms can fire.
What Chrome extension APIs changed in Manifest V3?
Key API changes: background pages → service workers (no persistent execution), chrome.browserAction → chrome.action (unified popup API), blocking webRequest → declarativeNetRequest (static rules), and scripting.executeScript replaces the old tabs.executeScript. The Manifest V3 also introduced new APIs: chrome.scripting for programmatic injection, chrome.declarativeNetRequest for network interception, and improved host_permissions handling for better user-controlled permission grants.