Plasmo vs WXT vs CRXJS vs Extension.js: what I would use in 2026
Research current to August 23, 2026.
Browser extension tooling has a category problem. People compare Plasmo, WXT, CRXJS, and Extension.js as if they were four versions of the same product, which they aren’t. WXT and Plasmo want to shape the application. CRXJS mainly teaches Vite how to build an extension. Extension.js keeps an explicit manifest and manages browser sessions. A custom Vite setup with Mozilla’s web-ext is one layer below that.
WXT is my default full framework. CRXJS is the low-level pick. Plasmo still has the nicest path if you’re building a React-heavy extension that injects real UI into pages. Extension.js is the one I keep looking at when a team wants manifest.json sitting in the repo and a runner that launches browsers for them.
The category mistake
| Tool | Category | What it wants to own |
|---|---|---|
| WXT | Full extension framework | Entrypoints, manifest generation, builds, browser targets, testing helpers, packaging, and submission workflows |
| Plasmo | React-first extension framework and SDK | Extension UI, content-script rendering, build conventions, messaging, storage, and publishing helpers |
| CRXJS | Vite plugin | Extension-aware bundling, manifest integration, assets, and HMR |
| Extension.js | Extension CLI and build workflow | Bundling, browser runners, HMR, packaging, and cross-browser manifest handling |
Vite plus web-ext | Custom toolchain | Only the pieces you decide to wire together |
Those labels are mine, but they match what the projects document. Mozilla’s web-ext runs, lints, builds, and signs extensions. It doesn’t replace a JavaScript bundler.
“CRXJS versus WXT” is a messy question because they don’t compete at the same layer. WXT comes with conventions. With CRXJS, your team writes them.
WXT: the default I would start with
WXT covers more of the boring work than anything else in this list, and it doesn’t lock you to React. It uses Vite and supports multiple frontend libraries. Entrypoints are file-based, and the manifest is generated from those files plus config. It can target Chrome, Firefox, Edge, Safari, and other Chromium browsers, Manifest V2 and Manifest V3. WXT 0.21.4 came out August 11, 2026.
Storage is built in. For messaging, the docs point at third-party wrappers instead of inventing a WXT-only protocol. Testing is better covered than in the other tools here: a Vitest plugin with a fake browser environment, plus separate Playwright guidance. wxt submit can package and submit updates to Chrome, Firefox, and Edge. You still create the first store listing by hand. Safari still needs the native Xcode wrapper and a manual publishing path.
The conventions have a cost. Entrypoint files are also build-time configuration, and WXT imports them in Node during the build. Browser-only code belongs inside the runtime entry function, not at module scope. WXT is still 0.x, and the versioning guide treats a bump of the second number as a major. WXT 0.21 raised the Node floor to 22, which knocks it out of a monorepo still pinned to Node 20. I would ship with it, pin the version, and read the upgrade notes.
Plasmo: still great at React, slower in the core
Content-script UI is still Plasmo’s strongest piece. You write React components that mount into existing pages, and you skip a week of shadow roots, style isolation, lifecycle glue, and extension plumbing. There are also packages for messaging and storage, live reload and React HMR, browser and manifest targeting, and the Browser Platform Publisher action for store submissions. Other view libraries work. React is what they optimized for.
WXT already has content-script UI primitives: integrated, shadow-root, and iframe-based interfaces, and it can mount on dynamic page elements. WXT’s own table is blunt: iframe UI gets HMR, integrated and shadow-root UI don’t. Plasmo’s edge is the React-centered loop, especially if injected UI is the product and you live in that HMR cycle all day.
It shows up in sidebars, page overlays, shopping helpers, research tools, and workflow assistants. In those apps, the injected interface is the product. Plasmo can delete a lot of unpleasant code.
The browser story is bumpier. Plasmo’s official support documentation lists Chrome Manifest V3 as the main target, Firefox Manifest V2 as supported, and Firefox Manifest V3 as experimental. Chromium forks should generally work. Safari is a conversion workaround, not a first-class Plasmo development and publishing workflow. BPP helps with Chrome, Firefox, and Edge submissions. The runtime differences are still there.
This is where I have mixed feelings. The repo still labels Plasmo as alpha, and the latest core release on GitHub is v0.90.5 from May 17, 2025. Some examples got 2026 updates, so people are still using it. The core cadence is still the problem. WXT, CRXJS, and Extension.js all shipped in 2026.
I wouldn’t migrate a healthy Plasmo app just because another framework is having a moment. For a new long-lived product, I’d ask whether the content-script UI saves enough work to justify the maintenance and browser-support tradeoffs. Sometimes it absolutely does.
CRXJS: for Vite teams that want a plugin
I like CRXJS for the same reason I like a good compiler plugin: it solves one ugly problem and then mostly disappears.
CRXJS makes Vite understand browser extensions: Manifest V3 builds, static asset imports, generated web-accessible resources, and real HMR in popups, background workers, and content scripts. The manifest can stay an explicit TypeScript or JSON file instead of being generated from a directory convention. CRXJS 2.7.0 landed on June 19, 2026 with MAIN-world content-script HMR. The earlier 2.6.1 release added full Vite 8 support and a broader compatibility test matrix.
There is no CRXJS messaging layer, storage API, dependency-injection system, test harness, or store publisher. If your team already lives in Vite, the app can stay a normal Vite project with a thin extension boundary.
One layout:
src/
background/
content/
popup/
options/
shared/
browser/
messaging/
storage/
types/
manifest.config.ts
vite.config.ts Put browser-specific APIs in shared/browser. Keep message contracts and storage schemas in your own modules. Then CRXJS stays a build-time dependency instead of leaking into every file.
WebExt Core is a set of small libraries for type-safe messaging, typed storage, alarm-based jobs, an in-memory fake browser, background proxy services, and isolated content-script elements. It fits CRXJS well. I would pull in a WebExt Core package when a native API gets annoying. A stack I would use:
CRXJS
+ @webext-core/messaging
+ @webext-core/storage
+ @webext-core/fake-browser
+ Vitest
+ Playwright
+ web-ext for Firefox linting/signing
+ publish-browser-extension or store-specific CI You still take on more work: cross-browser manifest differences, runtime API normalization, tests, packaging, store submission, architectural conventions. The main Vite plugin is Manifest V3. Manifest V2 lives in a separate Rollup plugin.
Extension.js: the middle ground I would watch
Extension.js keeps a normal manifest.json and supports browser-prefixed manifest keys. It runs managed browser binaries and profiles, and it does HMR across background scripts, content scripts, popups, and options pages. React, Vue, Svelte, Preact, and vanilla all work. A production build can write a zip. Runtime communication still uses native browser messaging APIs rather than a proprietary application layer.
CRXJS doesn’t launch browsers for you. Extension.js does, and you still keep manifest.json instead of WXT’s file-based entrypoints. It wants to own launching browsers and getting HMR right.
The project is moving fast. Version 4.1.4 came out August 22, 2026, after a dense run of v4 releases. Safari is now a CLI target on macOS with Xcode, but the support is still settling. preview and start don’t work, and there’s no live reload yet. The intro docs still treat Chrome, Edge, and Firefox as the core workflow. The npm README and the dedicated Safari guide advertise Safari. That looks like documentation lag: lots of commits, and some of the pages still catching up.
Fixes and new browser support show up quickly. You also eat more churn, and you have less years-in-production proof than WXT. I’d use Extension.js when an explicit manifest and a managed multi-browser runner matter more than WXT’s application model.
Raw Vite plus web-ext: when full control is worth the work
Use Vite, Rollup, or Webpack for bundling. Keep one or more explicit manifests. Add Mozilla’s web-ext for Firefox development, linting, packaging, and signing. The web-ext project is still active. Version 10.6.0 came out August 4, 2026.
This route makes sense for unusual build graphs, strict compliance, custom signing pipelines, or an extension stuck inside a monorepo that already has a toolchain. It also means your team owns reload behavior, manifest transforms, browser launchers, asset handling, test fixtures, packaging, and the weird edge cases a framework has already hit.
You depend less on any one framework, and you pay for that inside the team. Someone becomes the extension tooling maintainer, whether or not that title shows up on the org chart.
Other names you will run into
@samrum/vite-plugin-web-extension is another lower-level Vite option. It supports Manifest V2 and Manifest V3, content-script ESM, HMR, and multiple UI frameworks. I’d still pick CRXJS for a new project. It has explicit Vite 8 support and 2026 releases. The Sam Rum plugin is still worth a look if its content-script module model or existing compatibility fits better.
WebExtension Toolbox is a smaller CLI for building, packing, validation, and a few browser-extension conveniences. It helped define the category. It isn’t the first tool I’d reach for on a new 2026 app.
Projects such as create-chrome-ext are scaffolds. They save setup time and give you a starter structure. After the generator finishes, you still need a build strategy and an application architecture.
WebExt Core is a library layer, not a framework. I’d reach for it first if I started with CRXJS. It also fits inside a WXT app if you don’t want a WXT-specific messaging protocol.
A note on build speed
I wouldn’t pick among these from marketing claims about speed. WXT and CRXJS are Vite-based. Plasmo’s stack is Parcel-based. Extension.js documents an Rspack-based pipeline. They also do different amounts of work during development. A cold-build number from one starter template says almost nothing about a real extension with several content scripts, source maps, browser-specific manifests, and a large UI. Test against your own repo. Measure cold start, rebuild time, how the extension reloads, and what the production bundle looks like.
Day to day, reload quality usually matters more than a small gap in bundling time. A fast rebuild is wasted if the background worker didn’t restart or the content script kept stale state. “HMR” also means several different things: popup state, content-script swap versus tab reload, service-worker restart versus full extension reload, profile persistence, isolated-world versus MAIN-world scripts. CRXJS, WXT, Plasmo, and Extension.js all advertise better reload workflows. Their coverage and failure modes are different enough that a small proof of concept beats a generic benchmark.
Decision matrix
These ratings are mine. “Better” depends on the row.
| Criterion | WXT | Plasmo | CRXJS | Extension.js | Raw Vite plus web-ext |
|---|---|---|---|---|---|
| Abstraction level | Full framework | React-first framework and SDK | Vite plugin | CLI and build workflow | Custom stack |
| Manifest model | Generated from entrypoints and config | Mostly generated and abstracted | Explicit TS or JSON | Explicit JSON with browser prefixes | Fully explicit |
| Build engine | Vite | Parcel-based stack | Vite | Rspack | Your choice |
| Required Node baseline | Node 22 for WXT 0.21 | Node 16+ documented | Follows your Vite version | Node 22+ for Extension.js v4 | Your choice |
| Runtime helpers | Storage included; messaging external | Messaging, storage, and content-script UI | Bring your own; WebExt Core is the usual stack | Native browser APIs | Bring your own |
| Development reload model | UI HMR; CSUI HMR only for iframe UI | Strong React HMR for pages and CSUI | Vite HMR, including MAIN-world content scripts | Broad HMR with managed browsers | Depends on your setup |
| Browser reach | Broad, including Safari builds | Chrome strongest; Firefox and Safari caveats | Broad, with more manual work | Broad; Safari is settling | Whatever you implement |
| Manifest versions | MV2 and MV3 | Target-dependent, with uneven support | Main plugin is MV3; MV2 is separate | MV3 default with compatibility adapters | Whatever you implement |
| Testing | Best integrated story here | Mostly your own stack | Your own Vitest or Playwright setup | Documented Playwright workflow | Entirely your setup |
| Shipping | Zip and later submissions; first listing is manual | BPP for major stores | DIY | Zip built in; store workflow remains yours | DIY |
| Optional hosted platform | None required | Itero | None | extension.dev services | None unless selected |
| Framework lock-in | Medium | Medium to high | Low | Low to medium | Lowest |
| Recent maintenance | Active, August 2026 release | Watch closely, core release May 2025 | Active, June 2026 release | Very active, August 2026 release | web-ext active; the full stack is yours |
| Maintenance burden on your team | Low to medium | Low at first, higher when escaping conventions | Medium | Medium | High |
| Best fit | Most new production extensions | React-heavy injected interfaces | Experienced Vite teams | Explicit manifest plus managed browsers | Unusual or regulated builds |
The factual rows come from each project’s current docs and release history. Lock-in and maintenance burden are my calls, based on how much of the app model each tool owns.
What I would choose
| Situation | Pick |
|---|---|
| Most new production extensions | WXT |
| Vite team that wants architectural control | CRXJS, usually with WebExt Core |
| React-heavy interface injected into pages | Plasmo, if CSUI is the product |
| Explicit manifest plus managed browsers | Extension.js |
| Safari-first product | WXT today; Extension.js is worth testing |
| Strict or unusual build requirements | CRXJS first, custom stack only if CRXJS blocks you |
If you go CRXJS, wait on extra libraries. Plasmo still wants an early browser-target check, a pinned toolchain, and Plasmo APIs kept out of business logic. Extension.js Safari is experimental until live reload exists and the intro docs match the Safari guide.
Safari-first, I’d use WXT today. Publishing still needs Xcode and the native wrapper, but Safari is in the documented browser-target system. Plasmo is a conversion workaround. I wouldn’t bet a launch on Extension.js live reload that doesn’t exist yet. CRXJS leaves more of that path to you.
Going fully custom should solve a real constraint, not prove that the team knows how bundlers work.
The verdict
Is there a better extension framework than Plasmo or WXT? Sometimes you didn’t want a framework, and then the answer is CRXJS plus WebExt Core.