Skip to content

필사 모드: Browser Extension Development in 2026 — Manifest V3 / Plasmo / WXT / Side Panel API / Safari Extensions Deep Dive

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

Prologue — MV3 is here, for real this time

Browser extension developers heard "Manifest V3 will be enforced soon" for five years. Every time it was delayed. January 2022. January 2023. January 2024. And then in June 2024, Chrome finally kept its word.

Starting June 2024, MV2 extensions were disabled in the Chrome stable channel. New MV2 submissions to the Chrome Web Store had already been blocked a year earlier. In 2025, even the enterprise policy escape hatch (`ExtensionManifestV2Availability`) was phased out, and as of May 2026, MV2 is effectively a museum piece.

The weight of this change is not just "the manifest version went up."

- **Background Pages became Service Workers.** What used to be persistent pages are now event-driven workers that sleep and wake.

- **blocking webRequest was replaced by declarativeNetRequest.** uBlock Origin left Chrome and uBlock Origin Lite took its place.

- **Host permissions are runtime opt-in by default.** "Access to all sites" is no longer auto-granted at install time.

- **Side Panel API, Offscreen Documents API, Scripting API, declarativeNetRequest** — new APIs enable things that were impossible in the background-page era.

At the same time, extension tooling evolved. **Plasmo** made React a first-class citizen. **WXT** brought a Vite-based, lighter, faster alternative. **vite-plugin-web-extension** offers an even more minimal path. This article surveys the whole landscape.

Chapter 1 · Browser Extensions in 2026 — The MV3 Landscape

The big picture first.

Browser Extension API

|

+-------------------+-------------------+

| | |

Chrome (Blink) Firefox (Gecko) Safari (WebKit)

| | |

manifest_version: 3 manifest_version: 3 manifest_version: 3

service_worker background.scripts background.scripts

(event page) (persistent: false)

| | |

declarativeNetRequest webRequest blocking declarativeNetRequest

(MV2 compat kept) (limited webRequest)

| | |

chrome.* namespace browser.* + chrome.* browser.* (Promise)

Side Panel API sidebarAction partial support

Offscreen Documents backgroundScripts partial support

That single diagram captures the essence of 2026 extension development.

- **Chrome (and Chromium derivatives — Edge, Brave, Opera)**: MV3 only. No blocking webRequest.

- **Firefox**: Supports MV3 but keeps blocking webRequest in part (especially to support ad blockers). Background scripts use the event page model (sleep/wake).

- **Safari**: WebKit-based. Extensions work on iOS/iPadOS too (Safari 15+, iOS 15+). Some APIs unsupported.

All three browsers share the **WebExtensions** standard (W3C Browser Extensions Community Group) but diverge in the details. "Write once, run everywhere" is only half true.

The four components of an extension

+----------------------------------------------------+

| Extension Package (.crx / .xpi / .pkg) |

+----------------------------------------------------+

| manifest.json (metadata + permissions) |

| |

| +-----------------+ +-----------------+ |

| | Service Worker | | Content Script | |

| | (background) |<-->| (page-injected)| |

| +-----------------+ +-----------------+ |

| ^ ^ |

| | | |

| v v |

| +-----------------+ +-----------------+ |

| | Popup / UI | | Side Panel | |

| | (browser_action)| | (Chrome 114+) | |

| +-----------------+ +-----------------+ |

| |

| Offscreen Documents (when DOM is required) |

+----------------------------------------------------+

Each component is a separate execution context. They communicate via message passing (`chrome.runtime.sendMessage` and friends).

Chapter 2 · Manifest V3 — After the June 2024 deadline

The most basic manifest.json (Chrome MV3)

{

"manifest_version": 3,

"name": "My Extension",

"version": "1.0.0",

"description": "A simple MV3 extension",

"icons": {

"16": "icons/16.png",

"48": "icons/48.png",

"128": "icons/128.png"

},

"action": {

"default_popup": "popup.html",

"default_icon": "icons/16.png"

},

"background": {

"service_worker": "background.js",

"type": "module"

},

"content_scripts": [

{

"matches": ["https://*.example.com/*"],

"js": ["content.js"]

}

],

"permissions": ["storage", "tabs", "sidePanel"],

"host_permissions": ["https://api.example.com/*"],

"side_panel": {

"default_path": "sidepanel.html"

}

}

Key differences from MV2 to MV3

| Item | MV2 | MV3 |

| --- | --- | --- |

| Background | persistent page / event page | Service Worker (or event page on Firefox) |

| webRequest blocking | allowed | replaced by declarativeNetRequest (Firefox keeps it partially) |

| Host permissions | auto-granted at install | runtime opt-in possible (`activeTab`, `optional_host_permissions`) |

| Remote code | allowed | forbidden (all JS must be in the package) |

| Content Security Policy | object form | stricter object form |

| Action API | `browser_action` + `page_action` | unified `action` |

| Promise API | partial | all chrome.* APIs support Promises |

The remote-code ban is a big shift. Ad blockers can no longer dynamically eval remote filter lists, and analytics/experimentation tools that loaded JS from the cloud at runtime had to be reworked.

CRX format — how extensions are packaged

Chrome extensions are distributed as `.crx` files. They are essentially **ZIP files with a header**.

[CRX3 Magic: "Cr24"] [Version: 3] [Header Length] [Header (ProtoBuf)]

+- RSA signature (developer key)

+- ECDSA signature (Google or self)

[ZIP archive of extension files]

The signing key **determines the extension ID**. Sign with the same key and you get the same ID. Sign with a different key and you get a different one. Once you publish to the Web Store, Google manages that key for you.

Chapter 3 · Service Worker — Replacing the Background Page

The defining MV3 change is the new background context model.

Service Worker lifecycle

[event fires] -> [worker wakes] -> [handler runs] -> [idle detected]

|

v

[terminated after 30s]

Service Workers **can sleep**. After about 30 seconds of idle time the browser terminates them. They wake again on the next event.

What this implies:

- **You cannot keep state in global variables.** When the worker dies, they vanish. Use `chrome.storage.session` or `chrome.storage.local`.

- **Timers do not survive.** Use `chrome.alarms` instead of `setTimeout`/`setInterval`.

- **You cannot hold a long-lived WebSocket.** It will drop. If you need one, host it in an Offscreen Document.

- **No DOM.** Worker contexts cannot use DOM APIs. For parsing, use an Offscreen Document.

A typical service worker pattern

// background.ts

chrome.runtime.onInstalled.addListener(() => {

console.log('Extension installed')

chrome.storage.local.set({ installedAt: Date.now() })

})

chrome.action.onClicked.addListener(async (tab) => {

if (!tab.id) return

await chrome.scripting.executeScript({

target: { tabId: tab.id },

func: () => alert('Hello from extension!'),

})

})

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {

if (msg.type === 'PING') {

sendResponse({ type: 'PONG', timestamp: Date.now() })

}

return true // for async responses

})

// Periodic work via alarms

chrome.alarms.create('hourly-sync', { periodInMinutes: 60 })

chrome.alarms.onAlarm.addListener((alarm) => {

if (alarm.name === 'hourly-sync') {

syncData()

}

})

async function syncData() {

const res = await fetch('https://api.example.com/sync')

const data = await res.json()

await chrome.storage.local.set({ lastSync: data })

}

The keepAlive anti-pattern

Early on, developers confused by worker termination wrote `setInterval(() => fetch('/ping'), 20000)` to force their worker to stay alive. In 2026 that is a clear anti-pattern. Chrome will terminate such workers anyway, and policy discourages the practice. The proper approach is **store state in storage and wake on each event**.

Chapter 4 · declarativeNetRequest — Impact on Ad Blocking

The most controversial MV3 change. Blocking webRequest replaced by declarativeNetRequest (DNR).

Why the change

Chrome team's official position: **performance and privacy**. Blocking webRequest routed every network request through an extension worker for synchronous evaluation. That was slow, and extensions had overly broad visibility into all traffic.

The opposing view: stripping that power from extensions and constraining it to **declarative rules** means complex ad-blocking logic — especially cosmetic filters and dynamic blocking — got weaker.

Basic DNR usage

{

"manifest_version": 3,

"name": "Simple Ad Blocker",

"version": "1.0.0",

"permissions": ["declarativeNetRequest"],

"host_permissions": ["<all_urls>"],

"declarative_net_request": {

"rule_resources": [

{

"id": "ruleset_1",

"enabled": true,

"path": "rules.json"

}

]

}

}

[

{

"id": 1,

"priority": 1,

"action": { "type": "block" },

"condition": {

"urlFilter": "||doubleclick.net^",

"resourceTypes": ["script", "image", "sub_frame"]

}

},

{

"id": 2,

"priority": 1,

"action": { "type": "redirect", "redirect": { "url": "https://example.com/blocked" } },

"condition": {

"urlFilter": "||tracker.com^",

"resourceTypes": ["main_frame"]

}

}

]

Rules are **declarative**. The extension does not decide per-request; the browser consults the rule set.

Constraints

- **Static rule limits**: 30,000 per extension. 330,000 across all enabled extensions. The 1M+ rules of EasyList that uBlock Origin used to load do not fit.

- **Dynamic rules**: 5,000. User-added rules.

- **Session rules**: 5,000. Lost on browser exit.

- **Regex rules**: strict matching-time limits.

uBlock Origin's author Raymond Hill maintains a separate **uBlock Origin Lite** for MV3 Chrome but states explicitly that "it cannot match MV2 uBO." On Firefox, full-featured uBO still works (Mozilla chose to keep blocking webRequest).

Chapter 5 · Side Panel API (Chrome 114+)

Introduced in Chrome 114 (June 2023), the Side Panel API fundamentally changed extension UX. If a popup is an ephemeral UI that closes on click, the Side Panel is **a persistent UI alongside the browser**.

Basic usage

{

"manifest_version": 3,

"name": "Side Panel Demo",

"version": "1.0.0",

"permissions": ["sidePanel"],

"side_panel": {

"default_path": "sidepanel.html"

},

"action": {

"default_title": "Open Side Panel"

}

}

// background.ts

chrome.runtime.onInstalled.addListener(() => {

chrome.sidePanel

.setPanelBehavior({ openPanelOnActionClick: true })

.catch((error) => console.error(error))

})

// Activate the side panel only on certain tabs

chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {

if (!tab.url) return

const url = new URL(tab.url)

if (url.origin === 'https://www.example.com') {

await chrome.sidePanel.setOptions({

tabId,

path: 'sidepanel-example.html',

enabled: true,

})

} else {

await chrome.sidePanel.setOptions({

tabId,

enabled: false,

})

}

})

Why Side Panel matters

- **Persistence**: stays open across tab switches (or per-tab if you want).

- **Space**: popups cap around 800x600; side panels are much roomier.

- **Interaction**: keeps the panel visible while users work in the page.

- **Great fit for AI assistants**: chat UIs sit naturally in a side panel.

Edge supports side panels too (Edge's Copilot uses exactly this structure). Firefox has `sidebarAction`, a similar API that existed since MV2. Safari supports it in some versions.

Chapter 6 · Offscreen Documents + Scripting API

Offscreen Documents — when you need a DOM

Service Workers have no DOM. But extensions occasionally really need one — HTML parsing, clipboard access, audio playback, `DOMParser`, WebRTC, and so on.

Solution: **Offscreen Document**. You can run a hidden HTML page in the background.

{

"permissions": ["offscreen"]

}

// background.ts

async function ensureOffscreenDocument() {

const existing = await chrome.runtime.getContexts({

contextTypes: ['OFFSCREEN_DOCUMENT'],

documentUrls: [chrome.runtime.getURL('offscreen.html')],

})

if (existing.length > 0) return

await chrome.offscreen.createDocument({

url: 'offscreen.html',

reasons: ['DOM_PARSER'],

justification: 'Parse HTML for content extraction',

})

}

chrome.action.onClicked.addListener(async () => {

await ensureOffscreenDocument()

const response = await chrome.runtime.sendMessage({

target: 'offscreen',

type: 'PARSE_HTML',

data: '<html>...</html>',

})

console.log('parsed:', response)

})

<!-- offscreen.html -->

<!doctype html>

// offscreen.ts

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {

if (message.target !== 'offscreen') return

if (message.type === 'PARSE_HTML') {

const parser = new DOMParser()

const doc = parser.parseFromString(message.data, 'text/html')

const title = doc.querySelector('title')?.textContent ?? ''

sendResponse({ title })

}

})

`reasons` must be drawn from a fixed enum: `AUDIO_PLAYBACK`, `CLIPBOARD`, `DOM_PARSER`, `DOM_SCRAPING`, `IFRAME_SCRIPTING`, `LOCAL_STORAGE`, `TESTING`, `USER_MEDIA`, `WEB_RTC`, `BLOBS`, `WORKERS`.

Scripting API — the future of content scripts

MV2's `chrome.tabs.executeScript` was replaced by MV3's `chrome.scripting.executeScript`.

// Inject a function

await chrome.scripting.executeScript({

target: { tabId: 123 },

func: (color) => {

document.body.style.backgroundColor = color

},

args: ['lightblue'],

})

// Inject files

await chrome.scripting.executeScript({

target: { tabId: 123, allFrames: true },

files: ['content.js'],

})

// Insert CSS

await chrome.scripting.insertCSS({

target: { tabId: 123 },

css: 'body { filter: invert(1); }',

})

// Register content scripts dynamically

await chrome.scripting.registerContentScripts([

{

id: 'dynamic-script',

matches: ['https://*.example.com/*'],

js: ['dynamic.js'],

runAt: 'document_idle',

},

])

You need the `scripting` permission plus host permissions (or `activeTab`).

Chapter 7 · Plasmo — The React-first framework

Traditional extension development meant writing the manifest by hand, configuring webpack or rollup, wiring up hot reload yourself. Tedious. Plasmo unwinds it all **like Next.js**.

30-second setup

pnpm create plasmo my-extension

cd my-extension

pnpm dev

my-extension/

├── popup.tsx # automatically becomes the popup

├── options.tsx # automatically the options page

├── background.ts # automatically the service worker

├── contents/

│ └── plasmo.ts # content script

├── sidepanel.tsx # side panel

└── package.json # part of the manifest

No need to write manifest.json directly. It is generated from file naming and directory conventions. Metadata lives in package.json.

{

"name": "my-extension",

"displayName": "My Extension",

"version": "1.0.0",

"description": "...",

"manifest": {

"permissions": ["storage", "tabs"],

"host_permissions": ["https://*.example.com/*"]

}

}

Plasmo strengths

- **React-first**: `popup.tsx` is a React component out of the box.

- **HMR**: edit extension code and it reloads automatically.

- **Content Script UI**: drop React components into `contents/` and they get injected into the page.

- **Storage hook**: use `useStorage` via `@plasmohq/storage`.

- **Messaging**: type-safe message passing.

- **Cross-browser**: build separately for Chrome / Firefox / Edge.

Content Script UI example

// contents/inline.tsx

export const config: PlasmoCSConfig = {

matches: ['https://*.example.com/*'],

}

export default function ContentUI() {

const [count, setCount] = useState(0)

return (

)

}

This gets injected into the page and just works. Shadow DOM isolation is an option.

Plasmo weaknesses

- The abstraction is thick enough that fine-grained manifest control can be awkward.

- Build output tends to be heavy (React runtime included).

- Some community sentiment that BrowserStack (the parent) reduced backing. The OSS community remains active though.

Chapter 8 · WXT — The Vite-based newcomer

If Plasmo is the "React/Next.js" analogue, **WXT** is the "Vite/Nuxt" one. Lighter, faster, more flexible. It has grown rapidly through 2025-2026 since launching in 2023.

Basic structure

pnpm dlx wxt@latest init my-extension

cd my-extension

pnpm dev

my-extension/

├── entrypoints/

│ ├── background.ts

│ ├── content.ts

│ ├── popup/

│ │ └── index.html

│ └── options/

├── wxt.config.ts

└── package.json

// wxt.config.ts

export default defineConfig({

manifest: {

permissions: ['storage', 'tabs'],

host_permissions: ['https://*.example.com/*'],

},

modules: ['@wxt-dev/module-react'],

})

// entrypoints/background.ts

export default defineBackground(() => {

console.log('Hello from background!')

browser.runtime.onMessage.addListener((msg) => {

return Promise.resolve({ pong: true })

})

})

WXT strengths

- **Vite-based**: fast builds, ESM-friendly.

- **Framework-agnostic**: React module, Vue module, Svelte all possible.

- **Auto-imports**: Nuxt-style auto-imports — `browser`, `defineBackground`, `defineContentScript` are available as globals.

- **Cross-browser**: one codebase builds for Chrome/Firefox/Safari.

- **Web-ext integration**: dev mode auto-launches a browser with hot reload.

WXT vs Plasmo at a glance

| Item | Plasmo | WXT |

| --- | --- | --- |

| Foundation | Parcel/Next.js style | Vite/Nuxt style |

| Framework | React-centric (Vue/Svelte also possible) | All equal via modules |

| Manifest control | convention-first | direct authoring possible |

| Learning curve | low (just follow conventions) | medium (explicit configuration) |

| Community | active 2022-2024 then stalled | rapid growth 2024-2026 |

| Best for | quick React-focused prototypes | long-running, multi-framework, precise control |

For new projects in 2026, **WXT has the edge**. Plasmo is still good, but momentum shifted to WXT.

Chapter 9 · vite-plugin-web-extension — The minimal option

If framework weight bothers you, **vite-plugin-web-extension** may be the answer. One Vite plugin handles the extension build.

pnpm add -D vite vite-plugin-web-extension

// vite.config.ts

export default defineConfig({

plugins: [

webExtension({

manifest: path.resolve('manifest.json'),

additionalInputs: {

html: ['sidepanel.html'],

},

}),

],

})

Write `manifest.json` directly. Vite discovers entries and builds them. No convention-based automation, but **everything Vite offers is yours**.

How to pick

- "Lightweight, no framework dependency" — vite-plugin-web-extension

- "Vite ecosystem with some automation" — WXT

- "React-centric, move fast" — Plasmo

- "No framework, plain vanilla" — webpack/rollup yourself (not recommended now)

Chapter 10 · Firefox MV3 — Some MV2 retained

Mozilla had a subtle objection to Chrome's MV3 push. The result is that Firefox MV3 looks like Chrome MV3 but differs in places.

Firefox MV3 distinctions

- **Background**: event page model instead of Service Worker. Can sleep, but can hold a DOM.

- **Blocking webRequest**: retained. This is why uBlock Origin runs at full power on Firefox.

- **Host permissions**: runtime opt-in possible, like Chrome.

- **API namespace**: `browser.*` (Promise-based) is the standard. `chrome.*` works for compatibility.

Running the same code on both

// shared helper

const api = (typeof browser !== 'undefined' ? browser : chrome) as typeof browser

async function getCookie(url: string, name: string) {

return api.cookies.get({ url, name })

}

The `webextension-polyfill` library makes `chrome.*` return Promises.

await browser.storage.local.set({ key: 'value' })

Plasmo and WXT embed this polyfill or an equivalent abstraction.

Manifest differences

Firefox may require `browser_specific_settings` in `manifest.json`.

{

"manifest_version": 3,

"name": "Cross-Browser Ext",

"version": "1.0.0",

"background": {

"scripts": ["background.js"],

"type": "module"

},

"browser_specific_settings": {

"gecko": {

"id": "myext@example.com",

"strict_min_version": "115.0"

}

}

}

WXT handles this branching for you.

Chapter 11 · Safari Web Extensions — Mac/iOS

Apple adopted Web Extensions in Safari 14 in 2020. Starting with iOS 15 in 2021, extensions became possible on **iPhone and iPad**. That is not a minor thing — it is effectively the first viable mobile extension ecosystem.

Safari Web Extension characteristics

- **Wrapped in an Xcode project**: the web extension code lives inside an Xcode app container that you build. Distributed via the App Store.

- **Uses the `browser.*` API**: Mozilla-compatible namespace.

- **Some APIs unsupported**: `chrome.identity`, `chrome.sidePanel`, parts of `chrome.declarativeNetRequest`, `chrome.offscreen`, and others are missing or limited.

- **iOS constraints**: permission model is stricter on mobile. Users must explicitly activate.

Conversion tool — safari-web-extension-converter

To port an existing Chrome/Firefox extension to Safari:

xcrun safari-web-extension-converter /path/to/extension

This scaffolds an Xcode project. It reads the manifest and warns about unsupported APIs.

Distribution

- **macOS**: the Safari Extensions Gallery closed in 2022. All Safari extensions now ship through the **App Store**.

- **iOS**: same — via the App Store.

- **Apple Developer Program** membership required ($99/year).

This barrier is why Safari's extension ecosystem is not as rich as Chrome's. But it is the only way to reach iOS extension users, so it matters more and more.

Chapter 12 · Edge Add-ons — Microsoft

Edge is Chromium-based so Chrome extensions mostly just work. But Microsoft runs its own **Edge Add-ons** store.

Publishing to Edge

1. Register a developer account on Microsoft Partner Center (free).

2. Upload the same .zip package.

3. Review.

Compatibility with Chrome extensions

- **Almost 100%**: Chrome MV3 extensions run on Edge nearly unchanged.

- **Edge-specific APIs**: some are surfaced as `edge.*` instead of `chrome.*`, but mostly identical.

- **Sidebar**: Edge leans heavily on the Sidebar API (used by Copilot etc.).

Edge-only capabilities

- **Forced install policies**: IT admins can force-install extensions in enterprise environments.

- **Bing Chat / Copilot integration**: APIs that bridge Microsoft services.

- **Edge Workspaces**: extensions scoped to workspaces.

Edge sits at roughly 5-7% global share, which sounds small but **is comparable to Chrome in enterprise environments**. For B2B extensions, registering on the Edge store is essential.

Chapter 13 · AI in Extensions — Side Panel + LLM

The most interesting trend in 2026 extension development is **AI integration**. ChatGPT extensions, Perplexity, Glasp, Compose AI, Wisedocs — they all use the same pattern.

A typical AI extension architecture

[Web Page]

| (user action: text selection, summarize request)

v

[Content Script] -- extract page context -->

|

v

[Service Worker] -- call LLM API (Claude / OpenAI / self-hosted) -->

|

v

[Side Panel] -- stream the result -->

|

v

[User]

Side Panel + streaming UI

// sidepanel/App.tsx (WXT + React)

export default function SidePanelApp() {

const [messages, setMessages] = useState<{ role: 'user' | 'assistant'; content: string }[]>([])

const [input, setInput] = useState('')

async function send() {

const userMsg = { role: 'user' as const, content: input }

setMessages((m) => [...m, userMsg])

setInput('')

const response = await chrome.runtime.sendMessage({

type: 'ASK_LLM',

messages: [...messages, userMsg],

})

setMessages((m) => [...m, { role: 'assistant', content: response.text }])

}

return (

{messages.map((m, i) => (

{m.content}

))}

value={input}

onChange={(e) => setInput(e.target.value)}

onKeyDown={(e) => e.key === 'Enter' && send()}

className="flex-1 border p-2"

/>

Send

)

}

// background.ts

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {

if (msg.type === 'ASK_LLM') {

callLLM(msg.messages).then((text) => sendResponse({ text }))

return true // async

}

})

async function callLLM(messages: any[]) {

const apiKey = (await chrome.storage.local.get('apiKey')).apiKey

const res = await fetch('https://api.anthropic.com/v1/messages', {

method: 'POST',

headers: {

'x-api-key': apiKey,

'anthropic-version': '2023-06-01',

'content-type': 'application/json',

},

body: JSON.stringify({

model: 'claude-opus-4-7',

max_tokens: 1024,

messages,

}),

})

const data = await res.json()

return data.content[0].text

}

Using page context

// content.ts

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {

if (msg.type === 'GET_PAGE_TEXT') {

sendResponse({

title: document.title,

url: location.href,

text: document.body.innerText.slice(0, 50000),

})

}

})

// sidepanel: get current tab text and request a summary

async function summarizeCurrentPage() {

const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })

if (!tab.id) return

const pageData = await chrome.tabs.sendMessage(tab.id, { type: 'GET_PAGE_TEXT' })

// ... call the LLM

}

Chrome Built-in AI (Gemini Nano)

Starting in 2024, Chrome began exposing **on-device Gemini Nano** to extensions (Origin Trial then GA). The `chrome.ai` namespace is rolling out progressively, and as of May 2026 some APIs (prompt, summarize, translate) are stable.

// check availability

const availability = await ai.languageModel.availability()

if (availability === 'available') {

const session = await ai.languageModel.create()

const response = await session.prompt('Summarize: ...')

}

Pros: local, free, private. Cons: small model — limited for sophisticated work.

Business models

Four common business models for AI extensions.

1. **BYOK (Bring Your Own Key)**: users plug in their API keys. No traffic cost to the developer.

2. **Free quota + paid**: limited free use, then subscription.

3. **Pure SaaS**: extension is the client; auth and billing live in your backend.

4. **On-device**: Chrome Built-in AI or a quantized self-hosted model — zero cost.

Chapter 14 · Korea / Japan — Kakao, Naver, Pixiv

Korea

- **Kakao extensions**: utility extensions integrated with Kakao Talk share, Daum search, Kakao Bank auth, etc. Kakao does not officially publish an extension SDK, but many examples use Kakao APIs and OAuth from extensions.

- **Naver extensions**: enhanced Naver search results, Naver Cloud, Naver Dictionary, Naver Shopping price comparison (Signal, Danawa) extensions.

- **Toss / Karrot**: enterprise/internal extensions in payments and secondhand trading.

- **Translators**: Papago and Naver translation extensions. In mobile-heavy Korea, desktop translation is still a viable market.

- **Ad blockers**: AdBlock Plus with EasyList Korea is active.

Japan

- **Pixiv extensions**: pixiv image downloaders, tag enhancers, viewer helpers — many unofficial OSS projects. Be careful: pixiv ToS can conflict.

- **Niconico extensions**: video download helpers, comment enhancers, viewer improvements.

- **Mercari / Rakuma**: price-tracking extensions for secondhand marketplaces.

- **Rakuten extensions**: automatic points calculation, coupon auto-application.

- **Japanese learning**: Rikaikun, 10ten Japanese Reader, Yomichan (now forked as Yomitan) — effectively required for learners.

- **AdGuard**: Japanese EasyList Japan is actively maintained.

Kakao/Naver OAuth in extensions

// OAuth via chrome.identity.launchWebAuthFlow

async function loginWithKakao() {

const clientId = 'YOUR_KAKAO_REST_API_KEY'

const redirectUri = chrome.identity.getRedirectURL()

const authUrl =

`https://kauth.kakao.com/oauth/authorize?` +

`client_id=${clientId}&` +

`redirect_uri=${encodeURIComponent(redirectUri)}&` +

`response_type=code`

const responseUrl = await chrome.identity.launchWebAuthFlow({

url: authUrl,

interactive: true,

})

const code = new URL(responseUrl!).searchParams.get('code')

// ship the code to your backend to exchange for tokens

return code

}

The URL returned by `chrome.identity.getRedirectURL()` must be registered as a Redirect URI in the Kakao developer console. The shape is `https://<extension-id>.chromiumapp.org/`.

Realities of Korean/Japanese extension dev

- Direct calls into payment modules (Kakao Pay, Naver Pay, PayPay) from an extension are basically not feasible. A backend hop is required.

- In-app browsers (Kakao Talk, Line) do not support extensions. To reach mobile, Safari Web Extensions on iOS is roughly the only option.

- Japan is a conservative market with lower extension adoption than Korea or the US, but once installed, users tend to keep them for a long time.

Chapter 15 · Who should pick what — Solo / Team / Cross-browser / AI integration

Solo dev + quick prototype

- **Recommended stack**: Plasmo or WXT.

- **Why**: HMR, conventions, minimal boilerplate. You can ship a usable extension within a week.

- **Target browser**: Chrome first, expand to Firefox/Edge if it lands.

- **Notes**: Account for Web Store review (1-7 days on average).

Small team + long-running product

- **Recommended stack**: WXT.

- **Why**: Vite-based for fast builds, TypeScript-friendly, multi-browser support feels natural.

- **Recommendation**: UI regression tests via Storybook or Chromatic. End-to-end with Playwright's extension mode (load the extension into a BrowserContext).

Cross-browser first

- **Recommended stack**: WXT + `webextension-polyfill`.

- **Build matrix**: Chrome MV3, Firefox MV3, Edge MV3, Safari (separate Xcode project).

- **Notes**: For declarativeNetRequest, you may need to fall back to webRequest on Firefox.

AI-integrated extensions

- **UI**: Side Panel first. Popups only for short actions.

- **LLM**: start BYOK. If user count grows, consider your own backend.

- **On-device**: Chrome Built-in AI (Gemini Nano) is an option. Free is a powerful angle.

- **Context extraction**: content script extracts page text + metadata. Service Worker calls the LLM. Side Panel streams the output.

Ad blocking / content filtering

- **Chrome MV3**: doable within DNR limits. Full functionality is hard.

- **Firefox**: full functionality via blocking webRequest. Why uBlock Origin survives there.

- **Recommendation**: Firefox as the primary target, Chrome as a Lite branch.

Enterprise / B2B

- **Chrome**: enterprise policies for forced installation (`force_install`).

- **Edge**: same. Natural in Microsoft 365 environments.

- **Recommendation**: build a backend that can do SAML SSO against your company domain. Extension stays a client.

Chapter 16 · Extension Security — What to watch for

Keep host permissions minimal

{

"permissions": ["activeTab"],

"optional_host_permissions": ["https://*.example.com/*"]

}

`<all_urls>` is scrutinized in review. Prefer `activeTab`, and use `optional_host_permissions` for runtime-requested broader access.

Strict CSP

{

"content_security_policy": {

"extension_pages": "script-src 'self'; object-src 'self'"

}

}

`unsafe-eval` and `unsafe-inline` are forbidden in MV3. External CDN scripts are forbidden too (all JS must be packaged).

externally_connectable

To let web pages message your extension:

{

"externally_connectable": {

"matches": ["https://yoursite.com/*"]

}

}

This enables your own site to talk to the extension, but if the domain match is too broad it becomes a security hole.

XSS defense

- In content scripts, prefer `textContent`. Avoid `innerHTML`.

- React is safe by default, but be careful with `dangerouslySetInnerHTML`.

- Consider Trusted Types.

Privacy

- Store API keys in `chrome.storage.local`. Better yet, BYOK or token exchange through a backend.

- When sending page content externally, disclose explicitly and require opt-in.

- Follow Chrome Web Store's user data disclosure policy strictly — violation gets you delisted.

Chapter 17 · Chrome Web Store policies — How to get through review

Common rejection reasons

1. **Excessive permissions**: insufficient justification for `<all_urls>` or broad host permissions.

2. **Single-purpose violations**: each extension must have one clear purpose. Bundling utilities is grounds for rejection.

3. **Content policy**: injecting ads, manipulating search results, hijacking affiliates — strict no.

4. **User data policy**: when collecting data, a privacy policy URL is mandatory along with explicit disclosure.

5. **Source code obfuscation**: deliberate obfuscation is forbidden. Minification is OK but must remain readable.

6. **Payment circumvention**: limits on bypassing Chrome Web Store Payments via external payment.

Review timing

- **New submission**: 1-7 days on average, up to 2-3 weeks.

- **Updates**: typically 1-2 days.

- **Sensitive permissions (`<all_urls>`, `tabs`, `downloads`) lengthen the process.**

Beta channels

`unlisted` or `private` for beta testers first, then flip to public. Review happens again.

User data disclosure (Privacy Practices)

The Chrome Web Store requires explicit disclosure of:

- What user data you handle

- Why you handle it

- How you protect it

- Where you send it

False disclosure is immediate grounds for delisting.

Chapter 18 · Testing and CI/CD

Unit tests

// Vitest helper-function test

global.chrome = {

storage: {

local: {

set: vi.fn(),

get: vi.fn().mockResolvedValue({}),

},

},

alarms: {

create: vi.fn(),

onAlarm: { addListener: vi.fn() },

},

} as any

describe('syncData', () => {

it('saves data to storage', async () => {

await syncData()

expect(chrome.storage.local.set).toHaveBeenCalled()

})

})

E2E — Playwright with Extension

test('extension loads', async () => {

const pathToExtension = path.resolve(__dirname, '../.output/chrome-mv3')

const context = await chromium.launchPersistentContext('', {

headless: false,

args: [

`--disable-extensions-except=${pathToExtension}`,

`--load-extension=${pathToExtension}`,

],

})

const page = await context.newPage()

await page.goto('https://example.com')

// verify content script was injected

const injected = await page.evaluate(() => (window as any).__MY_EXT_LOADED__)

expect(injected).toBe(true)

await context.close()

})

CI/CD

.github/workflows/release.yml

name: Build and Release

on:

push:

tags: ['v*']

jobs:

build:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: pnpm/action-setup@v3

- uses: actions/setup-node@v4

with:

node-version: 20

cache: 'pnpm'

- run: pnpm install

- run: pnpm test

- run: pnpm build --target chrome-mv3

- run: pnpm build --target firefox-mv3

- uses: actions/upload-artifact@v4

with:

name: extensions

path: .output/

publish-chrome:

needs: build

runs-on: ubuntu-latest

steps:

- uses: actions/download-artifact@v4

- name: Upload to Chrome Web Store

uses: PlasmoHQ/bpp@v3

with:

keys: $TEST_KEYS_PLACEHOLDER

chrome-file: extensions/chrome-mv3.zip

The `PlasmoHQ/bpp` (Browser Plugin Publisher) action automates uploads to Chrome/Firefox/Edge.

Chapter 19 · Common pitfalls

"My Service Worker dies and the work gets cut off"

- Cause: terminated after 30 seconds idle.

- Fix: persist state to `chrome.storage`. Long work via `chrome.alarms` or `chrome.notifications` for progress.

"My WebSocket drops"

- Cause: Service Worker termination.

- Fix: host the WebSocket in an Offscreen Document and pass messages with the worker.

"Content script cannot access page variables"

- Cause: content scripts run in an **isolated world**. The page's `window` is not visible.

- Fix: use `chrome.scripting.executeScript` with `world: 'MAIN'`, or inject a script tag.

"Works on Chrome, broken on Firefox"

- Cause: API differences (especially DNR limits, host permission model).

- Fix: cross-browser builds in WXT/Plasmo, `webextension-polyfill`, branching code.

"Rejected in review — permissions too broad"

- Cause: `<all_urls>` or broad host permissions.

- Fix: `activeTab` first, only the domains you actually need.

"Extension was suddenly disabled — policy violation"

- Cause: missing data-use disclosure, single-purpose violation, etc.

- Fix: check the violation reason in the Chrome Web Store developer dashboard, fix it, and resubmit.

Chapter 20 · Epilogue — Are extensions dead or alive?

In the early 2020s people said "PWAs will replace extensions." As of 2026, that prediction is only half right. PWAs took some of the territory (installable web apps), but **the ability to interact with every page in the browser** belongs only to extensions. So extensions are not dead.

If anything, mandatory MV3 plus the AI wave triggered an **extension renaissance**. ChatGPT, Claude, and Perplexity extensions have millions of users, and new AI-assist extensions ship every day. The Side Panel API became the canvas for chat UIs, and being able to send page context freely to an LLM is a unique extension strength.

If we compress the 2026 extension-dev recommendation into one line:

> **Start with WXT or Plasmo. Learn Manifest V3, Service Workers, and the Side Panel API. Chrome first; Firefox/Edge via automated builds; Safari as a separate project if the market warrants. For AI integration, Side Panel + LLM API. Keep permissions minimal, follow review policies precisely, disclose user data honestly.**

That is the right answer for May 2026. A year from now it will change again — that is what the extension world is like.

References

- Chrome Extensions Manifest V3 — [https://developer.chrome.com/docs/extensions/mv3/intro/](https://developer.chrome.com/docs/extensions/mv3/intro/)

- MV2 to MV3 Migration — [https://developer.chrome.com/docs/extensions/migrating/](https://developer.chrome.com/docs/extensions/migrating/)

- Service Workers in Extensions — [https://developer.chrome.com/docs/extensions/develop/concepts/service-workers](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers)

- declarativeNetRequest API — [https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest](https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest)

- Side Panel API — [https://developer.chrome.com/docs/extensions/reference/api/sidePanel](https://developer.chrome.com/docs/extensions/reference/api/sidePanel)

- Offscreen Documents API — [https://developer.chrome.com/docs/extensions/reference/api/offscreen](https://developer.chrome.com/docs/extensions/reference/api/offscreen)

- Scripting API — [https://developer.chrome.com/docs/extensions/reference/api/scripting](https://developer.chrome.com/docs/extensions/reference/api/scripting)

- Chrome Built-in AI — [https://developer.chrome.com/docs/ai/built-in](https://developer.chrome.com/docs/ai/built-in)

- Plasmo — [https://docs.plasmo.com/](https://docs.plasmo.com/)

- WXT — [https://wxt.dev/](https://wxt.dev/)

- vite-plugin-web-extension — [https://github.com/aklinker1/vite-plugin-web-extension](https://github.com/aklinker1/vite-plugin-web-extension)

- Mozilla WebExtensions — [https://extensionworkshop.com/](https://extensionworkshop.com/)

- Firefox MV3 — [https://extensionworkshop.com/documentation/develop/manifest-v3-migration-guide/](https://extensionworkshop.com/documentation/develop/manifest-v3-migration-guide/)

- Safari Web Extensions — [https://developer.apple.com/documentation/safariservices/safari_web_extensions](https://developer.apple.com/documentation/safariservices/safari_web_extensions)

- Edge Add-ons — [https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/)

- webextension-polyfill — [https://github.com/mozilla/webextension-polyfill](https://github.com/mozilla/webextension-polyfill)

- uBlock Origin Lite — [https://github.com/uBlockOrigin/uBOL-home](https://github.com/uBlockOrigin/uBOL-home)

- Chrome Web Store Developer Program Policies — [https://developer.chrome.com/docs/webstore/program-policies/](https://developer.chrome.com/docs/webstore/program-policies/)

- PlasmoHQ BPP (Browser Plugin Publisher) — [https://github.com/PlasmoHQ/bpp](https://github.com/PlasmoHQ/bpp)

- Yomitan (formerly Yomichan) — [https://github.com/yomidevs/yomitan](https://github.com/yomidevs/yomitan)

현재 단락 (1/739)

Browser extension developers heard "Manifest V3 will be enforced soon" for five years. Every time it...

작성 글자: 0원문 글자: 34,652작성 단락: 0/739