Table of Contents
1. Why browser extensions are underrated
Browser extensions sit at a unique intersection: they have access to the pages your users already work in, they do not require a separate app install, and they can automate workflows that would otherwise require switching between multiple tabs and tools.
The market opportunity is significant. According to StatCounter (2026), Chrome holds approximately 65% of the global browser market share. The Chrome Web Store hosts over 125,000 extensions, and Chromium-based browsers (Edge, Brave, Opera, Arc) extend your potential reach even further. Extensions built for Chrome work across all of them with minimal modification.
For e-commerce businesses, this is particularly powerful. An extension can inject price comparisons directly into a competitor's page, automate bulk operations in marketplace dashboards, or add one-click invoicing to order management panels.
The ROI is often immediate: teams report saving 2-4 hours per day on repetitive browser-based tasks after deploying a custom extension. That is time that goes straight back into growing the business. For a deeper look at budgets and timelines, see our guide on the real cost of building a browser extension.
2. The stack: Vite + CRXJS + React + TypeScript
Our production stack has been refined over 100+ extension versions. Here is what we use and why. For a detailed comparison of extension frameworks, see our CRXJS vs Plasmo vs WXT comparison.
Vite 8
We run on Vite 8 (released March 2026) with backward support for Vite 3 through 7. Vite 8 ships Rolldown — a single Rust-based bundler replacing both esbuild and Rollup — delivering 10-30x faster production builds. Extension development involves constant reloading, and Vite makes this near-instant. No more waiting 30 seconds for webpack to rebuild. The new @vitejs/plugin-react v6 uses Oxc instead of Babel, further shrinking build times and install size.
CRXJS
The Vite plugin that understands Chrome extension structure. Created by Jack and Amy, it reads your manifest.json and handles content scripts, background workers, popup pages, and HMR automatically. We are contributors to this project (3.9k+ GitHub stars) and use it in every extension we ship. As Vite plugin authors ourselves, we understand the plugin API deeply and can extend the build pipeline when needed.
React
Component architecture translates perfectly to extensions. Popup UIs, option pages, and injected content script UIs are all just React components. Our internal react-content-script-injector library makes injecting React components into any page trivial.
TypeScript
Chrome API types catch errors at build time, not in production. The Chrome extension API surface is large and has many subtle type constraints - TypeScript surfaces these before your users do.
TanStack Query
Industry-standard data caching and server state management. In extensions, efficient caching is critical - you need fast access to API data without hammering endpoints or blocking the UI. TanStack Query handles cache invalidation, background refetching, and stale-while-revalidate patterns out of the box, so extension UIs stay responsive and data stays fresh.
This stack gives us hot module replacement in content scripts (yes, you can see changes injected into a live page without refreshing), type-safe messaging between extension contexts, and a development experience that feels like building a normal React app.
Vite config for Chrome extensions with CRXJS
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import crx from '@crxjs/vite-plugin';
import manifest from './manifest.json';
export default defineConfig({
plugins: [
react(),
crx({ manifest }),
],
});That is the entire build config. CRXJS reads your manifest.json and handles everything else.
Chrome extension framework comparison
| Feature | CRXJS | Plasmo | WXT |
|---|---|---|---|
| Bundler | Vite (native plugin) | Parcel | Vite (wrapper) |
| HMR in content scripts | Yes | Yes | Yes |
| Manifest ownership | You write manifest.json | Generated from code | wxt.config.ts |
| Framework lock-in | None (Vite plugin) | High (full framework) | Medium (Nuxt-like) |
| Best for | Teams who want control | Quick prototypes | Nuxt/Next.js developers |
Every extension we build is covered by end-to-end, integration, and unit tests. E2E tests verify the full user journey inside the browser, integration tests validate that content scripts, background workers, and popups communicate correctly, and unit tests lock down individual utilities and business logic. This layered approach catches regressions before they reach production.
3. Manifest V3: what actually changed
Manifest V3 (MV3) replaced V2 as the required format for Chrome extensions. Google has been sunsetting MV2, so all new extensions must use V3. Here are the changes that matter in practice:
Manifest V2 vs Manifest V3 comparison
| Feature | Manifest V2 | Manifest V3 |
|---|---|---|
| Background | Persistent background pages | Event-driven service workers |
| Network requests | webRequest API (blocking) | declarativeNetRequest (rules-based) |
| Content Security | Allows eval(), inline scripts | Strict CSP, no eval() |
| Remote code | Allowed | Prohibited |
| Promises | Callbacks only | Native promise support |
| Status | Deprecated (June 2025) | Required for all new extensions |
Background pages are gone. Service workers are event-driven and can be terminated by the browser at any time. This means you cannot rely on long-running state in the background - use chrome.storage or chrome.alarms instead.
The old webRequest API (for modifying network requests) is replaced by a declarative rules system. This is more restrictive but more performant. For most business extensions, this is not a blocker.
MV3 has stricter CSP defaults. No more eval() or inline scripts in extension pages. React and Vite handle this out of the box, so this rarely affects our workflow.
The practical impact: if you are building new, MV3 is just how extensions work now - and we are MV3 native. Every extension we build starts on Manifest V3 from day one, not as a migration from V2. If you are migrating an older extension from MV2, the service worker transition is the biggest hurdle. We have done this migration for multiple clients and can help if you are stuck.
Example manifest.json (Manifest V3 with CRXJS)
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"action": {
"default_popup": "src/popup/index.html"
},
"content_scripts": [{
"matches": ["https://example.com/*"],
"js": ["src/content/index.tsx"]
}],
"background": {
"service_worker": "src/background/index.ts",
"type": "module"
},
"permissions": ["storage", "activeTab"]
}4. Content scripts and injecting React into pages
Content scripts are the most powerful part of Chrome extensions. They run in the context of web pages, meaning they can read and modify the DOM of any page the user visits (with the right permissions).
The challenge is injecting a full React component tree into an existing page without breaking it. This is exactly what our internal react-content-script-injector library solves:
What react-content-script-injector does
- -Monitors injection points and automatically re-mounts components when the host SPA re-renders or navigates
- -Creates Shadow DOM containers with Tailwind CSS (or any styles) injected into the shadow root
- -Reports errors in real time across different users - feature flags and A/B tests on the host site never silently break your extension
- -Handles cleanup and re-injection on client-side route changes automatically
- -Provides positioning utilities (sidebar, floating panel, inline)
- -Handles React Portals so that the full React context (providers, state, theme) is always available - even inside Shadow DOM containers or injected panels
Static sites are the easy case - inject once and you are done. The hard part is React and SPA apps where the DOM is constantly changing, routes shift without page reloads, and the host app might be running A/B tests that move or rename the elements you are targeting. This is where we specialize, and why we built this library.
5. Shadow DOM: style isolation that works
Style isolation is the single biggest pain point in content script development. Without it, your extension UI inherits the host page's styles - buttons look wrong, fonts change, spacing breaks. Conversely, your extension's CSS can accidentally restyle the host page.
Shadow DOM provides true encapsulation. Styles inside the shadow root do not leak out, and styles outside do not leak in. This is critical for production extensions that need to work across thousands of different websites with different CSS resets, frameworks, and conflicting class names.
Our react-content-script-injector handles Shadow DOM setup automatically, including the tricky parts: injecting Tailwind into the shadow root, handling font loading across the shadow boundary, and making portals (modals, dropdowns) work correctly within the shadow context. Because the library manages React Portals natively, the full React context - providers, state, theme - stays available inside every portal, so components rendered in Shadow DOM behave exactly like components in a normal React tree.
6. Messaging between contexts
A Chrome extension has multiple isolated JavaScript contexts: the content script (runs on pages), the background service worker, the popup, and options pages. They communicate via the Chrome messaging API.
In practice, you end up with a pattern similar to a client-server architecture: the service worker is your "server" (handling API calls, storage, and business logic), and content scripts / popup are your "clients" that request data and trigger actions.
Our messaging best practices
- 1.Define typed message schemas (TypeScript discriminated unions work well)
- 2.Keep business logic in the service worker, UI logic in content scripts
- 3.Use
chrome.runtime.sendMessagefor request/response patterns - 4.Use
chrome.runtime.connectfor long-lived connections (streaming data) - 5.Always handle the case where the service worker is not yet initialized
With TypeScript, you can make messaging fully type-safe: a message of type "GET_PRICES" always returns a typed PriceData response. This eliminates an entire class of runtime errors that are hard to debug across contexts.
Type-safe messaging pattern
// messages.ts - Define typed message schemas
type MessageMap = {
GET_PRICES: {
request: { productId: string };
response: { prices: number[]; currency: string };
};
UPDATE_SETTINGS: {
request: { theme: 'light' | 'dark' };
response: { success: boolean };
};
};
// Type-safe sender (content script or popup)
async function sendMessage<T extends keyof MessageMap>(
type: T,
payload: MessageMap[T]['request']
): Promise<MessageMap[T]['response']> {
return chrome.runtime.sendMessage({ type, payload });
}
// Usage - fully typed, autocomplete works
const { prices } = await sendMessage('GET_PRICES', {
productId: 'sku-123'
});7. Storage and state management
Extensions have their own storage APIs: chrome.storage.local for device-local data and chrome.storage.sync for data that follows the user across devices. Unlike localStorage, these are available in all extension contexts (content scripts, service worker, popup).
For complex state, we use a lightweight reactive store pattern: the service worker holds the source of truth, and content scripts subscribe to changes via chrome.storage.onChanged. This gives you real-time sync across all open tabs without any additional infrastructure.
One important caveat: chrome.storage.sync has strict size limits (102,400 bytes total, 8,192 bytes per item). For larger datasets, use chrome.storage.local and handle cross-device sync yourself if needed.
Reactive storage pattern - sync state across all tabs
// content-script.ts - Subscribe to storage changes
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.userSettings) {
const newSettings = changes.userSettings.newValue;
// React component re-renders automatically
updateUI(newSettings);
}
});
// service-worker.ts - Write once, all tabs update
await chrome.storage.local.set({
userSettings: { theme: 'dark', language: 'en' }
});
// Every open tab receives the change event instantly8. Deployment and distribution
There are four main distribution channels for Chrome extensions:
Chrome Web Store (public)
The standard distribution channel. Requires a Google developer account ($5 one-time fee), review process (usually 1-3 business days), and adherence to Chrome Web Store policies. Good for tools with a broad audience.
Chrome Web Store (unlisted)
Same store, but the extension is not discoverable via search. Users need a direct link. Perfect for internal tools and client-specific extensions. Still goes through review.
Enterprise sideloading
For organizations with Google Workspace or managed Chrome browsers. Extensions can be force-installed via group policy without going through the store. No review required, instant updates.
Automatic tester installs from GitHub
We build custom installer apps that automatically push new extension versions to QA testers' machines. Every push triggers a CI/CD build, and our installer picks up the latest artifact from GitHub and updates the extension on the tester's browser automatically - no manual downloads, no dragging files, no store review. QA always runs the newest build without lifting a finger, shortening the feedback loop from days to minutes.
We handle the full deployment pipeline: build, package, submit, and iterate on review feedback. Our CI/CD automatically bumps extension and package versions, builds the artifact, and deploys directly to the Chrome Web Store - so every merge to main is a potential release. For testers, the same pipeline publishes installable builds to GitHub. For enterprise clients, we add group policy distribution on top. Read more about how we set this up in our CI/CD pipeline for browser extensions article.
9. Common mistakes we have seen
After building and reviewing dozens of extensions, these are the patterns that cause the most problems:
Storing state in the background page
With MV3 service workers, your background context can be killed at any time. Any in-memory state is lost. Always persist important state to chrome.storage.
Not handling SPA navigation
Many modern web apps use client-side routing. Your content script is injected once on page load, but the URL can change without a full reload. Use MutationObserver or the webNavigation API to detect these transitions.
Requesting too many permissions
Users see the permissions list before installing. Requesting "access to all websites" when you only need access to one domain will tank your install rate. Use the minimum permissions required and consider optional permissions for advanced features.
Ignoring the Chrome Web Store review process
Google reviews every extension update. If you use remote code execution, excessive permissions, or patterns that look like data collection, your update will be rejected. Know the policies before you build.
No style isolation in content scripts
Without Shadow DOM or equivalent isolation, your extension will look different on every website. We have seen extensions that work perfectly on one site and are completely broken on another because of CSS conflicts.
10. Frequently asked questions
How long does it take to build a Chrome extension?
A simple Chrome extension (popup-only, no content scripts) can be built in 1-2 weeks. A production extension with content scripts, background workers, API integrations, and proper testing typically takes 6-12 weeks. Complex extensions with features like real-time data syncing, multi-tab orchestration, or deep SPA integration can take 3-6 months. See our detailed breakdown in the real cost of building a browser extension.
Can you build a Chrome extension with React?
Yes. React works exceptionally well for Chrome extensions, especially for popup UIs, options pages, and content script UIs injected into web pages. With tools like CRXJS (a Vite plugin), you get hot module replacement and a development experience nearly identical to a standard React app. Our team has shipped 100+ production extension versions using React.
What is the difference between Manifest V2 and Manifest V3?
Manifest V3 replaces persistent background pages with event-driven service workers, replaces the blocking webRequest API with declarativeNetRequest, enforces stricter Content Security Policy (no eval or inline scripts), prohibits remote code execution, and adds native promise support. Google deprecated MV2 in June 2025, so all new extensions must use MV3. See the full comparison table above.
How much does it cost to publish a Chrome extension?
Publishing to the Chrome Web Store requires a one-time $5 Google developer registration fee. The store itself is free after that. Development costs vary widely: a focused MVP extension can start from around $1,200 (5,000 PLN), which is enough to validate your idea and ship a working product. Once the concept proves itself, you can invest further in scaling, advanced features, and production hardening.
Do Chrome extensions work on Edge and Brave?
Yes. Microsoft Edge, Brave, Opera, Vivaldi, and Arc are all Chromium-based browsers and support Chrome extensions natively. Extensions built with Manifest V3 work across all Chromium browsers with minimal or no modifications. Edge also has its own add-ons store where you can publish separately.
How do you debug a Chrome extension?
Chrome provides dedicated DevTools for extensions. Open chrome://extensions, enable Developer Mode, and click "Inspect views" to debug the service worker. For content scripts, use the regular page DevTools (F12) and look under the Sources tab. For popup pages, right-click the extension icon and select "Inspect popup." Using CRXJS with Vite adds hot module replacement, making the debug cycle much faster.
What happens when the host website changes and breaks my extension?
This is the most common issue with content script extensions that target specific websites. Host sites can change their DOM structure, CSS class names, or run A/B tests that move elements your extension depends on. Our react-content-script-injector library handles this by monitoring injection points and automatically re-mounting when the host page changes. It also reports errors in real time so you know immediately when something breaks. Read more about how agencies handle this in our article on browser extensions for agencies.
Related articles
CRXJS vs Plasmo vs WXT
Choosing the right Chrome extension framework in 2026
The Real Cost of Building a Browser Extension
Budgets, timelines, and what drives complexity
CI/CD for Browser Extensions
How CI/CD in CRXJS unlocked Windows development
Browser Extensions for Agencies
No API? No problem. How extensions bridge the gap
Need help building your extension?
We are CRXJS contributors with 100+ production extension versions shipped. Whether you need a new extension built from scratch or help migrating to Manifest V3, we can help.
