Numba in the Browser: Unlocking a New Scientific Python Stack in JupyterLite | Notebook.link Documentation
Skip to main content<br>A JIT compiler—and its ecosystem—running entirely in the Web browser
Scientists, students, and engineers use Jupyter notebooks to explore ideas interactively: write a small piece of code, execute it, inspect the result, and continue from there. Traditionally, every such notebook requires a Python process running on a server or on the user's machine.
JupyterLite changes this model. Its kernels run locally in the Web browser through WebAssembly, so a static website can provide a complete computational environment without allocating a server to every user. This makes notebooks easier and cheaper to share at scale, whether they are used for documentation, education, or interactive demonstrations.
There has, however, been an important piece missing from the browser-based scientific Python ecosystem: Numba .
Today, we are excited to share the first working version of the Numba JIT compiler running entirely in the browser with JupyterLite and emscripten-forge!
Numba in action in JupyterLite, showing a 249× speedup over standard Python.
In this example, Numba delivers a roughly 250× speedup in WebAssembly, compared with about 90× natively. The larger relative gain makes Numba especially compelling in the browser, where bypassing Python interpreter overhead can have an even greater impact.
This means that a Python function can be transformed into Numba's Intermediate Representation (IR), typed, lowered to LLVM IR, compiled into WebAssembly, dynamically linked, and executed—all without a remote Python server.
Try it here: Numba and its ecosystem in JupyterLite.
A long-standing request
Support for Numba in WebAssembly has been discussed for many years. The request appeared in the Numba project as early as 2018 in numba/numba#3284, while the Pyodide community tracked the packaging challenge in pyodide/pyodide-recipes#192.
The difficulty was not simply that Numba had never been packaged for WebAssembly. Numba is a compiler, and its execution model depends on llvmlite and LLVM. On a native platform, generated machine code can be placed into executable memory and called immediately. The browser deliberately does not allow applications to create or modify executable memory in this way.
Consequently, bringing Numba to the browser required more than adapting a build system or fixing a few platform checks. We needed a WebAssembly-aware execution engine for llvmlite, a way to invoke the LLVM linker inside the running browser process, and support for dynamically loading the generated code into the persistent Python runtime.
Fortunately, this was a problem we had encountered before.
From interactive C++ to llvmlite
Our earlier work on Xeus-Cpp in JupyterLite brought the Clang-Repl C++ interpreter to the browser. Clang-Repl cannot use LLVM's conventional JIT machinery under WebAssembly either, so we introduced a WebAssembly execution model with a different pipeline:
Compile the generated LLVM IR into a WebAssembly object file.
Link that object with wasm-ld into a WebAssembly side module—the WebAssembly equivalent of a dynamically loaded shared library.
Dynamically load the side module into the running application.
Resolve its symbols and call the compiled function.
Each new input incrementally extends the running program. The newly loaded side module shares memory with the main application, and its exported symbols can be used by modules loaded later.
This work is now the foundation of Xeus-Cpp in the browser. It is also the subject of our FOSDEM 2026 talk on interactive C++ workflows.
The central realization behind this project was that a similar architecture could be applied to llvmlite.
We implemented a WebAssembly execution engine that takes LLVM modules produced through llvmlite, emits WebAssembly objects, invokes LLVM's linker, LLD, through its in-process re-entrant driver, and loads each result as an Emscripten side module. Using LLD in process is essential: spawning a wasm-ld subprocess is not an option inside the browser.
The modules are loaded globally and kept alive, allowing runtime libraries, compiler-generated helpers, and user functions to resolve one another. This gives llvmlite the incremental execution behavior required by Numba while respecting the browser's security model.
Before moving further up the stack, we used this engine directly to compile, optimize, inspect, and execute LLVM IR. We also enabled in-process Graphviz rendering, making it possible to display control-flow graphs without launching the dot executable as a subprocess.
You can explore this lower-level pipeline here: llvmlite and Graphviz in JupyterLite.
Building LLVM IR and rendering its control-flow graph entirely in the browser.
Walking up the stack to Numba
Numba begins at a much friendlier level. A user writes an ordinary Python function and applies @jit or @njit:
from numba...