2025-11-04

Compiling React in the Browser using esbuild-wasm

A lot of people don't know, but it's possible to run a React application in the browser using just one file.

You can even compile your entire React app directly in the browser. No need for server-side builds.

First, let's see how we can render React using pure HTML and JavaScript.

We'll use an import map to resolve React and ReactDOM, by pointing to their corresponding ESM URLs. It's a hack: esm.sh has already done the dependency bundling work.1

<!DOCTYPE html>
<html>
  <head>
    <script type="importmap">
      {
        "imports": {
          "react": "https://esm.sh/[email protected]",
          "react-dom/client": "https://esm.sh/[email protected]/client"
        }
      }
    </script>
  </head>
  <body>
    <div id="root"></div>

    <script type="module">
      import React from 'react';
      import { createRoot } from 'react-dom/client';

      function App() {
        const [count, setCount] = React.useState(0);

        return React.createElement(
          'div',
          null,
          React.createElement('h3', null, 'Count: ', count),
          React.createElement(
            'button',
            { onClick: () => setCount(count + 1) },
            'Increment'
          )
        );
      }

      const root = createRoot(document.getElementById('root'));
      root.render(React.createElement(App));
    </script>
  </body>
</html>

If we place this code inside an iframe, a React application will be live and running. Try using Dev Tools to examine the network tab. You'll notice that React and ReactDOM are being loaded from ESM URLs:

This is awesome, but in practice nobody writes React by manually creating elements everywhere.

Instead, we typically write our components using JSX (or even better, TSX), but since browsers can't interpret JSX directly, we need to transform it into regular JavaScript first.

Transforming JSX with esbuild-wasm

One way to go from JSX to JavaScript is to use esbuild's esbuild.transform(), which can run in the browser using WebAssembly (WASM):2

import * as esbuild from 'esbuild-wasm';

await esbuild.initialize({
  wasmURL: 'https://unpkg.com/[email protected]/esbuild.wasm',
});

const jsxCode = `
function App() {
  return <h1>Hello, World!</h1>;
}
`;

const result = await esbuild.transform(jsxCode, {
  loader: 'jsx',
  target: 'es2015',
});

Here's an interactive editor that uses esbuild.transform() to perform the transformation in real time. Edit the code and see the JavaScript output update in no time.

Input (JSX)
Output (JavaScript)
// Initializing esbuild...

esbuild.transform() works great for a single piece of code, but it doesn't support resolving multiple files, custom imports, or bundling.

Compiling Multiple Files and Bundling

The esbuild.build() method can do all of that:

  • Resolves imports: Follows import statements and includes dependencies, allowing custom plugins to resolve and load.
  • Bundles code: Combines multiple files into a single output.
  • Tree-shakes: Removes unused code.
  • Handles external modules: Can mark certain modules (like React) as external and load them separately.

Here's how to set up esbuild.build() to handle path aliases and bundle local files:

// Define the virtual file system
const virtualFiles = {
  "@/entry": {
    contents: `...`,
    loader: "tsx",
  },
  "@/components/ui/button": {
    contents: `...`,
    loader: "tsx",
  },
  "@/lib/utils": {
    contents: `...`,
    loader: "js",
  },
};

const result = await esbuild.build({
  entryPoints: ["@/entry"],
  bundle: true,
  format: "esm",
  write: false,
  plugins: [
    {
      name: "virtual-files",
      setup(build) {

        // First, a resolver to mark React as external and resolve @/ paths to "virtual" namespace
        build.onResolve({ filter: /.*/ }, (args) => {
          // Mark React as external (loaded via importmap)
          if (/^(react|react-dom|react-dom/client)$/.test(args.path)) {
            return { path: args.path, external: true };
          }

          // Resolve all @/ paths to virtual namespace
          if (args.path.startsWith("@/")) {
            return {
              path: args.path,
              namespace: "virtual",
            };
          }
        });

        // Handles the "virtual" namespace to return file contents
        build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
          return virtualFiles[args.path] || null;
        });
      },
    },
  ],
});

const bundledCode = result.outputFiles?.[0]?.text;

For more details on esbuild's build options, see the official esbuild documentation.

Below is an editor powered by esbuild.build(). The code is compiled and bundled in the browser, and then rendered in an iframe.

File Structure
src/
entry.tsx
components/
ui/
button.tsx
lib/
utils.js
entry.tsx
Initializing esbuild...
Live Preview

Wrapping Up

Compiling React in the browser opens up exciting possibilities. You can build your own playground, a lightweight vibe-coding tool, or showcase animations alongside a code editor so readers can tweak examples. All in the user's browser!

As an example, check out this interactive React demo built with Motion, where you can modify and experiment with it live.

Code
Initializing esbuild...
Live Preview

The same technology is behind faces.app, a platform that that lets users generate software artifacts for storytelling. Give it a try!

1In this first setup, esm.sh is doing almost all the heavy lifting. It fetches React and other dependencies, transpiles them, and serves everything as ready-to-run ES modules. There's no local build step or bundling happening here; the browser just loads what esm.sh already prepared. Interestingly, esm.sh itself originally used esbuild under the hood, and that's exactly what we'll use next to bundle our own files and extend this example further.
2While esbuild-wasm is an excellent choice for browser-based transformation and bundling, other alternatives include Babel (extensive plugin support) and SWC (similar performance profile). There are many more options available, each with its own strengths.