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 sleepsMV3 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.executeScriptusing bundled functions - Offscreen documents — used the new
offscreenAPI for DOM-context operations like canvas stitching for full-page screenshots - Build pipeline — added Vite with
viteStaticCopyto 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.