Wasm Labs

Modern websites in a Raspberry Pi Zero with WebAssembly

By Angel M Miguel
At 2022 / 08 8 mins reading

Portability is one of the core features of WebAssembly (Wasm) allowing you to run modern applications in environments you could not before. Add sandboxing and security features to the mix and WebAssembly turns out to be a great platform to run applications in the browser and outside of it.

When talking about outside of the browser, how small can we go? There is a wide range of devices in the market. This includes everything from powerful cloud servers to low-resource devices such as the Raspberry Pi Zero W and the Pi Zero 2 W.

We wanted to experiment with running a modern software development stack on top of Wasm in one of these constrained devices. We built and run a modern website based on PreactJS, Vite, and MDX. The project includes features like Server Side Rendering (SSR) and client hydration.

The following is a quick video with the result. The rest of the article explains how you can try it on your own hardware. You can also find the code in our repository.

Image with Video thumbail. This will link you to a youtube video

Try it

If you are like most developers, you have a couple of Raspberry Pis in a drawer somewhere. This project in particular targets one of the low-end models: Raspberry Pi Zero.

To simplify the deployment, we created a tarball with all the code you need to run it. Just follow these steps to get your own server running in your Pi Zero:

  1. SSH into your Raspberry Pi Zero
    ssh pi@PI_ZERO_IP_ADDRESS
  2. Pull the pre-built tarball and extract the content
    wget https://github.com/vmware-samples/webassembly-browser-apps/releases/download/pi-zero-ssr%401.0.0/build.tar.gz
    tar xvf build.tar.gz
  3. Run the server
    # Pi Zero (32-bit):
    ./ssr-handler ./ssr.wasm ./dist
    # Pi Zero 2 (64-bit):
    ./ssr-handler-64 ./ssr.wasm ./dist
  4. Access the site on http://PI_ZERO_IP_ADDRESS:8080.

If you want to build the project instead of using the binaries, you can follow the instructions in the project README file.

Modern websites

Modern web component libraries and frameworks such as React and Vue rely heavily on JavaScript. They require users to download the JavaScript code to their browser, process it, and run it before getting any visible element.

To improve this situation, the web ecosystem started to look for hybrid approaches for rendering websites. Two well-known rendering strategies are:

  • Static Site Generation: process web components to store the output as static files (HTML, CSS, and JS for interactivity)

  • Server-Side Rendering: a server processes web components and sends the HTML back in the response. After loading the application in the browser, it will take control of the existing elements to add interactivity. This process is called hydration

As part of this project, we wanted to integrate Server-Side Rendering (SSR) in a Wasm module.

JavaScript in WebAssembly

JavaScript is not a compiled language, so it requires an interpreter to run the code. In other words, you need to embed a JavaScript interpreter along with the code in the Wasm module. Fortunately, there are multiple options like QuickJS and SpiderMonkey.

In this project, we included QuickJS. To simplify this process, we used Javy, a tool from the Shopify team. This open-source tool receives a JavaScript source code file as input and outputs a Wasm module with QuickJS and your source code. You must define the main function of your application. It will be called along with the QuickJS interpreter any time the module is run.

You can find this integration in the server.js file:

import render from "preact-render-to-string";
import App from "./App";

const renderer = props => {
const content = render(<App url={props.url} />);

// [...] Return the generated content back to the host
};

Shopify = {
main: renderer
};

Raspberry Pi Zero 1 and 2

A Raspberry Pi Zero 2 connected to a 7-inch screen. The console output shows the ARM 32-bit operating system after loading it

The Raspberry Pi Zero W and Pi Zero 2W are inexpensive, small devices. The former integrates a BCM2835 chipset (ARM 32-bit / 1 Core at 1GHz), while the Pi Zero 2W integrates a BCM2710A chipset (ARM 64-bit / 4 Cores at 1GHz). Both include 512MB of RAM.

As you may guess, such constrained devices represent a challenge for this kind of project. Especially, the Pi Zero W model and its ARM 32-bit CPU. There are not many WebAssembly runtimes that support this architecture. Most runtimes support 64-bit, as 32-bit represents an entirely different challenge.

For example, Wasmtime has an open issue to add support to ARM 32-bit. WasmEdge released the 0.8.1 version with ARM 32-bit support. However, it crashed on the Raspberry Pi Zero W, and the newer versions do not compile to this architecture successfully.

So, what are the options we have here?

Running WebAssembly on ARM 32-bit

After looking at the WebAssembly runtime alternatives, we selected the Wazero project. This runtime is written entirely in Golang, which has built-in support to compile to ARM 32-bit.

We decided to give it a try and it worked as a charm. The Raspberry Pi Zero W was able to run a WebAssembly example module and print the response.

We still required an HTTP server to receive requests and call the module. We built it on top of the Golang project and included an HTTP handler and a static file server.

Performance

Web services are typically sensitive to performance. You want to minimize the time it takes to process user requests. These boards are designed to provide a fully capable system while consuming a fraction of the power required for a regular server. The expectations around performance should be low.

From the tests we made, these are the average response times to render the index page of the project:

Path Avg time on Pi Zero (PiOS 32-bit) Avg time on Pi Zero 2 (PiOS 32-bit) Avg time on Pi Zero 2 (PiOS 64-bit)
/ 16s 6s 190ms
/about 6.5s 2.5s 110ms

The Pi Zero 2 is considerably faster and can work as a local server. However, the Pi Zero is way less performant, so there is a performance gap to fix until we consider it ready for local usage.

ARM 32-bit and Wazero

We observed that the size of the JavaScript code impacts the performance of the Pi Zero drastically. Adding a few more components and pages may increase the rendering time by ten times (x10).

Takeshi Yoneda, an engineer from the team behind the Wazero project (Tetrate), explained to us that the project runs in 32-bit thanks to the cross-compilation feature from Golang. Internally, Wazero uses a compiler to optimize binary before running it. This compiler is not available on this specific architecture, so the Pi Zero cannot take advantage of its optimizations. Instead of that, the code runs directly through the interpreter, which is not optimized for performance.

We will be going deeper into this topic. Expect more articles comparing architectures and trying to run WebAssembly in multiple environments.

Compile once and run everywhere

The ability to write source code that can compile and be run securely in different environments is an incredible advantage. Managing multiple codebases increases the effort of maintaining them, augments the attack surface, and slows down development processes.

The way WebAssembly works, only two pieces are needed to run workloads: a WebAssembly runtime and a module. While remaining compatible, runtimes provide a wide variety of performance and functionality tradeoffs.

Even if they get more optimized, devices like the Pi Zero will still have their limitations. However, thanks to Webassembly, they can still be relevant as specialized nodes instead of generalist ones. The low price and low energy requirements make them an excellent alternative for running partial workloads.

If you are interested in our work, do not forget to follow us on @vmwwasm to get updates on our projects.

Do you want to stay up to date with WebAssembly and our projects?