Announcing WinterJS
The most performant JavaScript Service Workers server thanks to Rust and SpiderMonkey
Founder & CEO
October 27, 2023
Follow up the WinterJS series reading about the recent production-ready WinterJS 1.0 release here.
Today we are incredibly excited to announce WinterJS (wasmer/winterjs
package).
WinterJS is a JavaScript Service Workers server written in Rust, that uses the SpiderMonkey engine to execute JavaScript (the same engine that Firefox uses). We chose to follow the WinterCG specification to aim for maximum compatibility with other services such as Cloudflare Workers, Deno Deploy and Vercel (hence the name WinterJS).
WinterJS is not only blazing fast™️ but can also be compiled to WebAssembly thanks to WASIX and thus also run fully with Wasmer.
Let's see how it works. We'll start by creating a simple serviceworker.js
file that just returns a simple "hello world";
addEventListener('fetch', (req) => {
req.respondWith(new Response('hello'));
});
Running it with WinterJS is as simple as this:
$ wasmer run wasmer/winterjs --net --mapdir /app:. /app/serviceworker.js
WinterJS can also be run natively with Rust (
cargo install --git https://github.com/wasmerio/winterjs && winterjs serviceworker.js
). You can find the source code of WinterJS in the GitHub repo: https://github.com/wasmerio/winterjs
Thanks to the WASIX capabilities of WinterJS, the JavaScript service worker can also be deployed to Wasmer Edge. Check the working demo here: https://wasmer-winter-js-sample-worker.wasmer.app/
And now that you have seen a sneak peak on how to use WinterJS, lets do a deep dive on our journey building it.
Choosing the JS engine
Before starting on the quest of creating a JavaScript Service Workers server, we analyzed the Javacript runtimes that we could use.
Here are the main requirements we have for such JavaScript runtime:
- Speed: It should be fast to run
- Wasm-compatible: It should be able to run without restrictions in a Wasm environment (such as Wasmer)
- Development time: We should be able to iterate fast on it
And here are the JS runtimes that we analyzed:
- QuickJS. Challenges:
- We will need to implement all the JS apis diff (
peformance.now()
,addEventListener
, …) - We need to implement the serviceWorker API entirely in C
- We will need to implement all the JS apis diff (
- Static Hermes. Challenges:
- Node.js polyfills not available in static mode
- Not a lot of functions (such as http calls) are available in the polyfill
- Had to make it compile to Wasm with WASIX
- Bun (JavascriptCore). Challenges:
- Zig not fully supporting WASIX
- Compiling JavascriptCore to WASIX is possible (was done before), but not trivial
- MozJS (SpiderMonkey). Challenges:
- We will need to implement all the JS apis diff (
peformance.now()
,addEventListener
, …) - We need to implement the serviceWorker API (in Rust)
- We will need to plug the service worker with a WASIX http server
- We will need to implement all the JS apis diff (
- Node.js (v8). Challenges:
- Compile v8 in jitless mode to Wasm is an unknown-unknown
Using SpiderMonkey with mozjs
After a few runtime trials we set on SpiderMonkey as the most reasonable approach that fitted our tight timeline.
So we begin porting. We started with a fork of mozjs that supported a new compilation tier called PBI (Portable Baseline Interpreter).
After some work on the mozjs build system to target WASIX, we were able to bypass most of the issues, except one: the bindings generation.
The bindings that allow using the SpiderMonkey C++ API from Rust were automatically generated using c-bindgen. Plugging those bindings onto WASIX was a challenge so we simply decided to target a 32 bit system and modify them by hand (a 32,000 file!) to target WASIX.
And voilá… everything worked!
However, after adding a few missing resources to JS, we realized that perhaps mozjs didn’t have the easiest API to use:
unsafe extern "C" fn base64_encode(cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool {
let args = CallArgs::from_vp(vp, argc);
if args.argc_ < 1 {
return false;
}
let source = js_try!(cx, raw_handle_to_string(cx, args.get(0)));
let result = ::base64::engine::general_purpose::STANDARD.encode(bytes);
rooted!(in(cx) let mut rval = UndefinedValue());
result.to_jsval(cx, rval.handle_mut());
args.rval().set(rval.get());
true
}
Using SpiderMokey with spiderfire
Thankfully, the spiderfire project had been working on improving the API surface for using SpiderMonkey from Rust.
So the example laid out before now looks way simpler and easier to read/maintain:
#[js_fn]
fn btoa<'cx>(val: String) -> String {
let bytes = val.as_bytes();
::base64::engine::general_purpose::STANDARD.encode(bytes)
}
Deploying to Wasmer Edge
Compiling WinterJS to WASIX was challenging, but completely worth it. Thanks to its WASIX capabilities we can now run any Javascript Service Workers workloads in Wasmer Edge.
We have put together an in depth tutorial on how to use Javascript Service Workers in Wasmer Edge... please check it out!
https://docs.wasmer.io/edge/quickstart/js-wintercg
We believe WinterJS will enable many new use cases. For example, running Service Workers natively in your IoT device (where Node is too heavy to run), or even in your browser.
At Wasmer we are incredibly excited to see how you will use WinterJS.
WinterJS on GitHub: https://github.com/wasmerio/winterjs
About the Author
Syrus Akbary is an enterpreneur and programmer. Specifically known for his contributions to the field of WebAssembly. He is the Founder and CEO of Wasmer, an innovative company that focuses on creating developer tools and infrastructure for running Wasm
Founder & CEO
Choosing the JS engine
Using SpiderMonkey with mozjs
Using SpiderMokey with spiderfire
Deploying to Wasmer Edge
Read more
engineeringwasmer edgeedge computing
The Cloud is dead, long live the Cloud! Announcing Wasmer Edge
June 15, 2023
wasmerwasmer edgerustprojectsedgeweb scraper
Build a Web Scraper in Rust and Deploy to Wasmer Edge
RudraAugust 14, 2023
wasmer runtimejavascriptwasmer-jsbrowser
Introducing the new Wasmer JS SDK
Michael BryanDecember 13, 2023