conda in the browser#
conda-express includes cx-wasm, a WebAssembly build of the same rattler-based
solver and extractor used by the native cx CLI. Combined with the
conda-emscripten plugin, this enables real conda install to run entirely
client-side in a JupyterLite notebook — no server required.
This is not a reimplementation. The actual conda CLI (conda.cli.main) runs in
Python compiled to WASM, with cx-wasm replacing conda’s native-code bottlenecks
(solver, extractor, repodata fetching) via conda’s plugin API.
Try it#
Open the live demo and run:
%load_ext conda_emscripten
%conda install lz4
import lz4
Both %conda and %cx are available as IPython magics. All conda subcommands
work — install, list, remove, search, info, etc.
How it works#
Architecture#
Browser tab
└── JupyterLite (main thread)
└── cx-jupyterlite extension (rewrites bare "conda" → "%cx")
└── xeus-python kernel (WebWorker)
└── Python 3.13 (WASM/Emscripten)
├── cx_wasm_bridge (shard prefetch at startup)
└── conda (real conda, compiled to WASM)
└── cx-wasm plugins (Rust → WASM)
├── solver: rattler/resolvo (replaces libsolv)
├── repodata: CEP-16 sharded fetch (msgpack.zst)
└── extractor: streaming .conda/.tar.bz2 → MEMFS
Startup: async shard prefetch#
When the kernel starts, cx_wasm_bridge.setup() runs a shard prefetch of
sharded repodata before the user types anything. This is the key to fast solves:
Collects package names from
conda-meta/(the pre-installed environment)Calls Rust (
cx_get_shard_urls) to compute shard URLs from the cached indexFetches all shard URLs in parallel via JavaScript
fetch()APIDecodes each shard with Rust (
cx_decode_shard_deps) to discover dependenciesQueues newly discovered dependencies for the next level
Repeats until no new dependencies are found
All fetched shards are cached in a JavaScript Map. When the solver later
requests a shard via sync XHR, it reads from this cache instead of making a
network request.
Command execution#
When you run %conda install lz4:
The
%condamagic parses the command, auto-injects--yes, and snapshots existing.sofiles in the prefixOn first use,
_bootstrap_prefix()createsconda-meta/,.condarc, and sets environment variables (CONDA_ROOT_PREFIX,CONDA_SUBDIR, etc.)Runtime patches are applied for Emscripten’s MEMFS limitations (no
seek(), nosubprocess, nofcntl.lockf)conda.cli.main.main("install", "lz4", "--yes")runs — real condaThe
CxWasmSolverdelegates solving to cx-wasm’s Rust resolvo solver via the JS bridge — shards come from the prefetch cache, so the solve is pure computation with no network I/OPackages are downloaded via sync XHR (patched
download_inneravoidsseek())Archives are extracted by cx-wasm’s Rust extractor (with
Uint8Arrayconversion forwasm-bindgencompatibility)After the command completes, newly installed
.sofiles are found and loaded viactypes.CDLLwithRTLD_GLOBALso C extensions work immediately
Performance#
Phase |
Typical time |
Notes |
|---|---|---|
Prefetch (kernel startup) |
~2-4 s |
Async, parallel; before user interaction |
Solve |
~0.2 s |
Pure computation against cached shards |
Download + extract |
~0.3 s |
Per-package, sequential |
Transaction (link) |
~1.5 s |
File copy in MEMFS |
Shared lib load |
~0.1 s |
|
Total ( |
~3.5 s |
The prefetch runs during kernel startup, overlapping with the time the user
spends opening or writing notebook cells. By the time a %conda install command
runs, all repodata shards are already cached.
Packages#
Packages come from emscripten-forge — the same packages as native conda, compiled to WebAssembly. Both pure Python packages (from conda-forge) and C extension packages (from emscripten-forge) work.
Local development#
To build and test the JupyterLite demo locally:
# Prerequisites: pixi, wasm-pack
# Build the cx-wasm WASM module
pixi run -e web wasm-build
# Build the conda packages (cx-wasm-kernel, conda-emscripten)
pixi run -e recipes build-cx-wasm-kernel
pixi run -e recipes build-conda-emscripten-plugin
# Build and serve the JupyterLite site
pixi run -e lite lite-build-local
pixi run -e lite lite-serve
# Open http://localhost:8888/lab/index.html
The --with-local flag in lite-build-local adds the locally-built packages
from output/ to the JupyterLite environment. Without it, only public
emscripten-forge packages are included.
Components#
Component |
Location |
Role |
|---|---|---|
cx-wasm |
|
Rust solver + extractor + shard decode, compiled to WASM |
conda-emscripten |
|
conda plugin: solver, extractor, magics, patches |
cx-jupyterlite |
|
JupyterLite extension: intercepts bare |
cx-wasm-kernel |
|
WASM files + Python bridge + shard prefetch for xeus-python |
JupyterLite site |
|
Static site builder and demo notebooks |
Limitations#
MEMFS is volatile — installed packages don’t persist across page reloads
No subprocess — post-link scripts are silently skipped
No symlinks or hardlinks — MEMFS doesn’t support them
Network required — packages are fetched from emscripten-forge CDN at runtime
Platform — only
emscripten-wasm32packages from emscripten-forge are available