How to Run Node.js in the Browser

How to Run Node.js in the Browser

·

9 min read

By Andy Smith

Running code securely in a browser sandbox unleashes Node.js

Get ready to unleash the power of Node.js in the browser as we break free from remote VMs and local binaries by running our code securely in a browser sandbox.

We will focus on two relatively new technologies, WebContainers by StackBlitz and Nodebox by Codesandbox, with a view to seeing:

  • How feasible and usable these tools are and what underlying technologies they use

  • What features and limitations they have

  • If it’s worth trying to build our own apps or integrations around them

With this in mind, our main goal is to run Fastify in the browser from our own codebase and allow users to experiment with it in real-time. You can find the final result here.

Before we dive in, it’s important to note that there are several online tools available that allow you to run your code with Node.js in the browser without having to set up anything locally:

Browser sandboxing

Web applications often run third-party code like ad scripts or user-generated content to provide their full range of features. But running untrusted code on a web page can be a risk, as it can potentially access sensitive user information or execute malicious code.

If we can provide a sandboxed environment for running untrusted code in the browser, and we can isolate the code from the rest of the web page and the underlying system, then, theoretically, we can prevent it from accessing sensitive information, modifying the DOM, or executing or accessing anything malicious.

File management

Both WebContainers and Nodebox use different mechanisms to sandbox the code being run in the browser and emulate a local filesystem by means of a Javascript object containing key/value pairs to represent filenames and their contents.

This is where we hit our first important hurdle when it comes to using these tools. Each system requires a ‘files content’ to be provided as a raw escaped string.

const webContainerFiles = {
  'folder/file.js': {
      contents: "console.info('hello world')"
  },
  'README.md': {
    contents: "# Hello world"
  }
}

const sandpackFiles = {
  "/folder/file.js": "console.info('hello world')",
  '/README.md': '# Hello world'
}

Both systems:

  • Do not provide a way to work with the file system for security (and likely proprietary) reasons. If you want to work with the file system or a remote endpoint and save your work, you must implement your own functionality.

  • Use an in-memory virtual file system, which can be interacted with by users. However, without extensive initial work, the implementation is limited to the browser and changes will be lost on refresh.

Regardless of the approach used for editing, whether it involves keeping files in their original state, creating new files, or utilising local storage or the Web’s Filesystem API, we must initially provide the raw files to each tool.

Both products have comprehensive documentation for getting started that we won’t replicate here, but each guide uses Vite to get up and running, so we’ll assume we’re adopting its usage here.

💡 Some quick tips for importing files with Vite:

// To import the raw contents of a single file as string use:
import serverFile from './server.js?raw'

// If you want to import an entire directory with all files/file types then we can use:
const allFiles = import.meta.glob('../path/**/*', { as: 'raw' })

// This returns an object keyed with the filename and relative path
// With a promise value that returns the files raw contents as a string when invoked
const result = {
    '../path/subfolder/README.md': () => Promise<string>
}

For more information see:

What are WebContainers?

WebContainers are a browser-based runtime for executing Node.js applications and operating system commands, entirely inside your browser tab.

Stackblitz has been working together with the teams at Google and Next.js and offers excellent in-depth documentation and guides to get started. You can find an online playground on the linked page to run it.

This will open Stackblitz, inside of which a copy of a Vite application will be running inside a WebContainer, that in turn runs Node in a WebContainer with the output of the page in an iFrame.

Notable features

  • Container Management: Supports the creation, starting, stopping, and deletion of containers.

  • Volume Management: Endpoints for managing volumes, which can store data persistently across container instances.

  • Load Balancing: Developers can distribute traffic across multiple container instances.

  • Auto-Scaling: Endpoints for managing auto-scaling, which can be used to automatically spin up additional containers to handle increased traffic.

  • Webhooks: Can be used to receive notifications when events occur within the platform.

Sounds amazing! But, before we get too excited, there are some important limitations we need to consider:

Using JavaScript’s sharedArrayBuffer

The sharedArrayBuffer is a JavaScript object that represents a fixed-length binary data buffer that can be shared between different execution threads. It is used to make web applications that require concurrent processing, such as audio and video processing, graphics rendering, and machine learning, faster.

By sharing memory, these applications can avoid the overhead of copying data between threads and reduce the need for synchronisation mechanisms.

However, this feature has been disabled by default in many web browsers since 2018 due to security concerns related to Meltdown and Spectre-style attacks.

To use the sharedArrayBuffer in a web application, developers need to explicitly opt-in and use a Content Security Policy (CSP) and disable access to the buffer from untrusted code.

Setting up cross-origin isolation

COOP (Cross Origin Opener Policy) and COEP (Cross Origin Embedder Policy) are two web security standards that can be used with Service Workers to prevent cross-site attacks and protect user privacy. COOP controls the level of isolation between different origins, while COEP protects users from cross-site data leaks. For an in-depth look, check out this great article on web.dev.

To set up WebContainers, we need to supply the required headers in our Vite config with the instruction below:

export default defineConfig({
  server: {
    headers: {
      'Cross-Origin-Embedder-Policy': 'require-corp',
      'Cross-Origin-Opener-Policy': 'same-origin',
    },
  },
})

This is problematic because if we want to serve a static page on, say, GitHub pages, we can’t control the required headers to be served. A potential workaround is to use a service worker like the COI Service Worker, but this is far from ideal.

Without rolling our own server implementation, this limits us to serving the application with some sort of backend service.

Browser compatibility

Because WebContainers make use of the sharedArrayBuffer extensively, there is limited browser support that is mostly scoped for use in Chromium.

From Stackblitz:

“For WebContainers, we support desktop Chromium-based browsers out of the box… and Firefox [in alpha state]”.

This unfortunately means that Safari is currently unsupported. You can find more detailed information here.

What is Nodebox?

Not to be confused with https://www.nodebox.net/ (a suite of design tools) Codesandbox’s Nodebox is a custom, closed source runtime that is run entirely in the browser.

From Codesandbox (a comprehensive overview can be found here):

“Nodebox is a runtime for executing Node.js modules in the browser”.

When using Nodebox with the default Codesandbox React offering, it feels like a more complete solution out of the box than WebContainers, especially when it comes to building our own experiences. Codesandbox has an excellent suite of components that you can use and it takes away the pain of managing the containers’ lifecycle.

Like WebContainers, though, it’s possible to roll your own fully customised experience without their ecosystem and implement it with Nodebox directly.

Notable Features

  • Node.js-compatible runtime with minimal performance impact.

  • Implements most of the Node.js API and uses browser API where applicable.

  • It has an internal dependency manager for optimal initial load time that uses Sandpack CDN, an open-source Rust package manager.

  • Nodebox is optimised for running sandboxes quickly with little friction.

  • Lack of access to operating system-level APIs.

Under the hood, it appears Nodebox provides its own stubbed-out version of the Node.js http server package which, upon experimentation, can present a few issues. For example:

  • You will be unable to run tools like nodemon for the entry point of scripts.

  • Dependency management is silent under the hood. Packages can be added, but not removed reliably.

  • Unlike other supported systems, Nodebox does not offer the watching/live recompilation/rerun of code without rolling your own mechanism which is very limited in scope.

Creating… something

It’s important to consider how these tools can be applied in practical ways to create something that is genuinely useful. For example, they could be used to:

  • Showcase a product

  • Integrate into an existing application

  • Provide a custom playground for developers to learn and experiment

As stated at the beginning of this article, our main goal is to run Fastify in the browser. To that end, we created a simple online playground running Fastify.

Rather than jump through hoops to get WebContainers working on a simple GitHub page because of the use of sharedArrayBuffer, the playground was built with Sandpack React.

Prerequisites: If you block third-party cookies by default (and you really should), then you may need to add some domain exceptions to work with Nodebox.

Final thoughts: these systems are very interesting, but…

TLDR: Both WebContainers and Nodebox are promising and interesting tools. However, these tools being closed source is somewhat problematic and both come with some important technical limitations.

Whilst super interesting and offering a cool developer experience, these systems aren’t practical enough to use in commercial projects yet without investing significant time into them.

Both WebContainers and Nodebox have been opened up to some degree, allowing developers to build their own applications and integrations. However, it is important to be mindful of vendor lock-in and to create experiences that are not directly tied to one sandboxing technology. While this is possible today, it requires implementing a lot of functionality from scratch, which incurs a high maintenance overhead.

Both tools make use of telemetry and, on occasion, load remote assets under the hood, so have the potential to leak sensitive work you’re experimenting with (that you shouldn’t due to licensing, confidentiality, NDAs, etc…). Also, if you’re using an ad-blocker then this could potentially impact your experience, as well as the use of third-party cookies coupled with local storage.

It is important to remember that whilst free for use on open source projects, both tools are proprietary systems run by for-profit companies. This is not a bad thing but you really should look into the licensing and usage aspects of both if you’re considering developing any type of commercial application.

Overall, we seem to have a very promising future despite their caveats. It is of course worth noting that having a healthy competition between the two is great for the ecosystem.

Let’s hope for native open source implementations in the future 🤞.