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.
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.
2. The stack: Vite + CRXJS + React + TypeScript
Our production stack has been refined over 100+ extension versions. Here is what we use and why:
Vite 8
We run on Vite 8 with backward support for Vite 3, 4, 5, 6, and 7. Lightning-fast builds and native ES module support. Extension development involves constant reloading - Vite makes this near-instant. No more waiting 30 seconds for webpack to rebuild. Staying on the latest version means you always get the newest optimizations and security patches.
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.
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:
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.
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.
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.
8. 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.
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.
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.