Vite in the browser
Tags: tooling, vite, webdev, javascript
Published: 2022-01-19 17:39:06

{% collapsible TL;DR %}

We made [browser-vite](https://github.com/divriots/browser-vite) - a patched version of [Vite](https://vitejs.dev/) running in the browser with Workers.

{% endcollapsible %}

## How it works - in a nutshell

- A [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API): replaces Vite's HTTP server. Capturing the HTTP calls of an embedded iframe from example.
- A [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers): Run [browser-vite](https://github.com/divriots/browser-vite) to process off the main thread.
- Calls to the file system are replaced by an in-memory file system.
- Import of files with special extensions (`.ts`, `.tsx`, `.scss`...) are transformed.

## The challenges

### No real File System

[Vite](https://vitejs.dev/) does a lot with files. The files of the project but also config files, watchers, and globs. These are difficult to implement in the browser with a shimmed in-memory FS. We removed watchers, globs, and config file calls to limit the complexity and the surface API.

The project files stay in the in-memory FS that [browser-vite](https://github.com/divriots/browser-vite) and vite plugins can access normally.

### No "node_modules"

[Vite](https://vitejs.dev/) relies on the presence of `node_modules` to resolve dependencies. And it bundles them in a [Dependencing Pre-Bundling](https://vitejs.dev/guide/dep-pre-bundling.html) optimization at startup.

We didn't want to run a `node_modules` folder in the browser's memory because we think it's just too much data to download and store into the browser's memory. So we carefully stripped out node resolvers and [Dependencing Pre-Bundling](https://vitejs.dev/guide/dep-pre-bundling.html) from [Vite](https://vitejs.dev/).

Users of [browser-vite](https://github.com/divriots/browser-vite) have to create a [Vite plugin](https://vitejs.dev/guide/api-plugin.html) to resolve bare module imports.

Our products: [Backlight.dev](https://backlight.dev), [Components.studio](https://components.studio) and [WebComponents.dev](https://webcomponents.dev), are running a server-side bundler optimizer for the past 2 years now. We created a Vite plugin for [browser-vite](https://github.com/divriots/browser-vite) to resolve node dependencies automatically. As of the date of this post, this server-side bundler is not open-sourced.

### Regex "lookbehind"

Some regexs in [Vite](https://vitejs.dev/) are using [lookbehind](https://www.regular-expressions.info/lookaround.html). This works great locally when executed by [Node.js](https://nodejs.org/en/), but [it's not supported in Safari](https://caniuse.com/js-regexp-lookbehind).

So we rewrote the regexs for more browser compatibility.

### Hot Module Reload (HMR)

[Vite](https://vitejs.dev/) uses [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) to communicate code changes from the server (node) to the client (browser).

In [browser-vite](https://github.com/divriots/browser-vite), the server is the ServiceWorker + Vite worker and the client is the iframe. So we changed the communication from [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) to a post message to the iframe.

For this, the client side code of Vite in iframe has been replaced by [a special browser version](https://github.com/divriots/browser-vite/blob/browser-vite/packages/vite/src/client/browser.ts) handling messages outside of [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API).

## How to use it

As of the time of this writing, it's not a plug and play process. There is a lot to figure out by reading Vite's internal processing in order to use [browser-vite](https://github.com/divriots/browser-vite).

Note: This post may become obsolete over time, so make sure you check
[browser-vite's README](https://github.com/divriots/browser-vite/blob/browser-vite/README.md#usage) for always up to date information on browser-vite's usage.

### Installation

Install the [browser-vite npm package](https://npmjs.org/browser-vite).

```bash
$ npm install --save browser-vite
```

or

```bash
$ npm install --save vite@npm:browser-vite
```

To channel "vite" imports to "browser-vite".

### iframe - window to browser-vite

You need an [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) that will show the pages served internally by [browser-vite](https://github.com/divriots/browser-vite).

### Service Worker - the in-browser web server

The Service Worker will capture certain URLs requests coming from the iframe.

Here is an example using [workbox](https://developers.google.com/web/tools/workbox).

```js
workbox.routing.registerRoute(
  /^https?:\/\/HOST/BASE_URL\/(\/.*)$/,
  async ({
    request,
    params,
    url,
  }: import('workbox-routing/types/RouteHandler').RouteHandlerCallbackContext): Promise<Response> => {
    const req = request?.url || url.toString();
    const [pathname] = params as string[];
    // send the request to vite worker
    const response = await postToViteWorker(pathname)
    return response;
  }
);
```

Mostly posting a message to the "Vite Worker" using [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) or [broadcast-channel](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API).

### Vite Worker - processing request

The Vite Worker is a Web Worker that will process requests captured by the Service Worker.

Example of creating a Vite Server:

```js
import {
  transformWithEsbuild,
  ModuleGraph,
  transformRequest,
  createPluginContainer,
  createDevHtmlTransformFn,
  resolveConfig,
  generateCodeFrame,
  ssrTransform,
  ssrLoadModule,
  ViteDevServer,
  PluginOption
} from 'browser-vite';

export async function createServer(
  const config = await resolveConfig(
    {
      plugins: [
        // virtual plugin to provide vite client/env special entries (see below)
        viteClientPlugin,
        // virtual plugin to resolve NPM dependencies, e.g. using unpkg, skypack or another provider (browser-vite only handles project files)
        nodeResolvePlugin,
        // add vite plugins you need here (e.g. vue, react, astro ...)
      ]
      base: BASE_URL, // as hooked in service worker
      // not really used, but needs to be defined to enable dep optimizations
      cacheDir: 'browser',
      root: VFS_ROOT,
      // any other configuration (e.g. resolve alias)
    },
    'serve'
  );
  const plugins = config.plugins;
  const pluginContainer = await createPluginContainer(config);
  const moduleGraph = new ModuleGraph((url) => pluginContainer.resolveId(url));

  const watcher: any = {
    on(what: string, cb: any) {
      return watcher;
    },
    add() {},
  };
  const server: ViteDevServer = {
    config,
    pluginContainer,
    moduleGraph,
    transformWithEsbuild,
    transformRequest(url, options) {
      return transformRequest(url, server, options);
    },
    ssrTransform,
    printUrls() {},
    _globImporters: {},
    ws: {
      send(data) {
        // send HMR data to vite client in iframe however you want (post/broadcast-channel ...)
      },
      async close() {},
      on() {},
      off() {},
    },
    watcher,
    async ssrLoadModule(url) {
      return ssrLoadModule(url, server, loadModule);
    },
    ssrFixStacktrace() {},
    async close() {},
    async restart() {},
    _optimizeDepsMetadata: null,
    _isRunningOptimizer: false,
    _ssrExternals: [],
    _restartPromise: null,
    _forceOptimizeOnRestart: false,
    _pendingRequests: new Map(),
  };

  server.transformIndexHtml = createDevHtmlTransformFn(server);

  // apply server configuration hooks from plugins
  const postHooks: ((() => void) | void)[] = [];
  for (const plugin of plugins) {
    if (plugin.configureServer) {
      postHooks.push(await plugin.configureServer(server));
    }
  }

  // run post config hooks
  // This is applied before the html middleware so that user middleware can
  // serve custom content instead of index.html.
  postHooks.forEach((fn) => fn && fn());

  await pluginContainer.buildStart({});
  await runOptimize(server);

  return server;
}
```

Pseudo code to process requests via [browser-vite](https://github.com/divriots/browser-vite)

```js
import {
  transformRequest,
  isCSSRequest,
  isDirectCSSRequest,
  injectQuery,
  removeImportQuery,
  unwrapId,
  handleFileAddUnlink,
  handleHMRUpdate,
} from 'vite/dist/browser';

...

async (req) => {
  let { url, accept } = req
  const html = accept?.includes('text/html');
  // strip ?import
  url = removeImportQuery(url);
  // Strip valid id prefix. This is prepended to resolved Ids that are
  // not valid browser import specifiers by the importAnalysis plugin.
  url = unwrapId(url);
  // for CSS, we need to differentiate between normal CSS requests and
  // imports
  if (isCSSRequest(url) && accept?.includes('text/css')) {
    url = injectQuery(url, 'direct');
  }
  let path: string | undefined = url;
  try {
    let code;
    path = url.slice(1);
    if (html) {
      code = await server.transformIndexHtml(`/${path}`, fs.readFileSync(path,'utf8'));
    } else {
      const ret = await transformRequest(url, server, { html });
      code = ret?.code;
    }
    // Return code reponse
  } catch (err: any) {
    // Return error response
  }
}
```

Check [Vite's internal middlewares](https://github.com/vitejs/vite/tree/main/packages/vite/src/node/server/middlewares) for more details.

## How does it compare to Stackblitz WebContainers

> "[WebContainers](https://blog.stackblitz.com/posts/introducing-webcontainers/):
> Run Node.js natively in your browser"

[Stackblitz](https://stackblitz.com/)'s [WebContainers](https://blog.stackblitz.com/posts/introducing-webcontainers/) can also run [Vite](https://vitejs.dev/) in the browser. You can elegantly go to [vite.new](https://vite.new) to have a working environment.

We are not experts in WebContainers but, in a nutshell, where [browser-vite](https://github.com/divriots/browser-vite) shims the FS and the HTTPS server at the Vite level, WebContainers shims the FS and a lot of other things at the [Node.js](https://nodejs.org/en/) level, and Vite runs on it with a few additional changes.

It goes as far as storing a `node_modules` in the WebContainer, in the browser. But it doesn't run `npm` or `yarn` directly because it would take too much space (I guess). They aliased these commands to [Turbo](https://developer.stackblitz.com/docs/platform/turbo) - their package manager.

[WebContainers](https://blog.stackblitz.com/posts/introducing-webcontainers/) can run other frameworks too, like [Remix](https://blog.stackblitz.com/posts/remix-runs-on-webcontainers/), [SvelteKit](https://blog.stackblitz.com/posts/sveltekit-supported-in-webcontainers/) or [Astro](https://blog.stackblitz.com/posts/astro-support/).

> It's magical ✨ It's mind-blowing 🤯 We have massive respect for what the Stackblitz team has built here.

One downside of [WebContainers](https://blog.stackblitz.com/posts/introducing-webcontainers/) is that it can [only run on Chrome today](https://developer.stackblitz.com/docs/platform/browser-support) but will probably [run on Firefox soon](https://developer.stackblitz.com/docs/platform/browser-support#testing-on-firefox). [browser-vite](https://github.com/divriots/browser-vite) works on Chrome, Firefox and Safari today.

In a nutshell, [WebContainers](https://blog.stackblitz.com/posts/introducing-webcontainers/) operates at a lower level of abstraction to run [Vite](https://vitejs.dev/) in the browser. [browser-vite](https://github.com/divriots/browser-vite) operates at a higher level of abstraction, very close to [Vite](https://vitejs.dev/) itself.

Metaphorically, for the retro-gamers out there, [browser-vite](https://github.com/divriots/browser-vite) is a little bit like [UltraHLE](https://en.wikipedia.org/wiki/UltraHLE) 🕹️😊

(\*) [gametechwiki.com: High/Low level emulation](https://emulation.gametechwiki.com/index.php/High/Low_level_emulation)

## What's next?

[browser-vite](https://github.com/divriots/browser-vite) is at the heart of our solutions. We are progressively rolling it out to all our products:

- [Backlight.dev](https://backlight.dev)
- [Components.studio](https://components.studio)
- [WebComponents.dev](https://webcomponents.dev)
- [Replic.dev](https://replic.dev) (New app coming up very soon!)

Going forward, we will continue to invest in [browser-vite](https://github.com/divriots/browser-vite) and report back upstream. Last month, we also announced that [we sponsored Vite via Evan You and Patak](/blog/supporting-vitejs) to support this wonderful project.

## Want to know more?

- GitHub Repository: [browser-vite](https://github.com/divriots/browser-vite)
- Join our [Discord server](https://discord.gg/XkQxSU9), we have a #browser-vite channel going on 🤗