Inside the slicer
A resin slicer in a browser tab
Yes. The same window as the desktop build runs in a browser tab, and a 998 000-face plate goes from the Slice button to a downloaded file in 1.2 s. What it costs is the printer network, a nightly toolchain and two response headers.
The slicer runs in a browser tab. Not a viewer and not a cut-down page: the same window as the desktop build, the viewport and every panel, loading a plate and handing you a sliced file at the end.
On an eight-core machine a 998 000-face sphere went from the Slice button to a downloaded file in 1.2 s. The desktop build took 1.24 s on one thread, and 0.72 s on seven. So a tab runs about level with the desktop app on a single core, and the desktop app with every core to itself is still a little under twice as quick.
What you give up is mostly the printer. A browser cannot reach a machine on your network, so the sliced file downloads and goes over on a stick or through the printer’s own app. Nothing warns you before you close the tab, and the page does not update itself the way the desktop build does.
If you want the evidence and the mechanism rather than the answer, the rest of this page is the measurements with their dates and the commands behind them, why every job runs on a worker and the page’s own thread never does, what the module weighs, and what the tab cannot do and why.
The measurements
The plate that takes 1.2 s in a tab is a 998 000-face sphere of 30 mm radius on an Elegoo Mars 4 Ultra profile: 8520 x 4320 panel, .goo out, 0.05 mm layers. Measured on 2026-10-04 in headless Chromium on an eight-core machine, on a build made from the repository with cargo xtask web.
| Run | Threads | Slice button to file |
|---|---|---|
| Browser, headless Chromium | 7 workers | 1.2 s |
| Native | 1 thread | 1.24 s |
| Native | 7 threads | 0.72 s |
Seven workers in the page come out level with one native thread: threading buys back what WebAssembly charges, and not much more. Seven and not eight because the pool is held to cores - 1, so the window keeps drawing while a job runs, and cores is whatever navigator.hardwareConcurrency reports.
That run is also the weakest evidence on this page. Our note records 8 cores and headless Chromium, names neither the CPU nor the operating system, and there is no headless harness in the repository to drive the window again, so nobody can reproduce those three figures from a command we publish. Read them as ratios on one machine. The table below is the one you can rerun.
The pipeline alone, on six plates
The pipeline also builds for a browser on its own: a .encrust project in, a sliced file out, one thread, no file system, no clock. slice_project is plain Rust, so the native and the WebAssembly columns are one function on two targets rather than two implementations.
Measured on 2026-10-03 on an Elegoo Saturn 4 Ultra panel (11520 x 5120) at 0.05 mm, supports grown from automatic points, a 2 mm wall where hollowed, the 1 GB cavity budget the browser asked for then, 8 cores, Node 24:
| Plate | Faces | File | Native, 1 thread | Native, 8 | Wasm, 1 | Native live | Wasm memory |
|---|---|---|---|---|---|---|---|
| Figure, 32 mm | 0.97 M | 6.7 MB | 0.7 s | 0.4 s | 1.0 s | 105 MB | 126 MB |
| the same, hollowed | 7.3 MB | 2.6 s | 1.1 s | 4.1 s | 184 MB | 257 MB | |
| Bust, 85 mm | 1.01 M | 177 MB | 5.6 s | 2.2 s | 7.4 s | 305 MB | 596 MB |
| the same, hollowed | 275 MB | 27.4 s | 6.3 s | 34.6 s | 575 MB | 1138 MB | |
| Stand, 85 mm | 0.87 M | 188 MB | 4.9 s | 2.1 s | 6.8 s | 308 MB | 587 MB |
| the same, hollowed | 253 MB | 12.9 s | 3.8 s | 20.8 s | 313 MB | 665 MB |
One WebAssembly thread is 1.3 to 1.6 times one native thread across those six rows.
The three hollowed rows are an upper bound, and the files they were taken from will not open today. When the run was taken, opening a project re-ran the Hollow tool and the automatic support run from the manifest. A project now holds the plate as it stands, builds nothing on opening, and its reader refuses any version but the one this build writes. Save those plates again and the hollowed rows come out faster, with smaller files.
The two memory columns are not the same quantity either. Natively it is the live bytes the allocator counts at the peak; in WebAssembly it is the linear memory after the run, which only ever grows, so it is the peak plus whatever the allocator could not reuse.
Both of those columns come from these two commands:
RAYON_NUM_THREADS=1 cargo run --release -p web-engine --example measure -- plate.encrust
node crates/web-engine/www/measure.mjs crates/web-engine/www/pkg plate.encrust
The second runs the module under Node’s V8 rather than in a page: Chromium’s engine, none of the page’s rules. The heavy rows are the plates whose sliced file is a growing Vec in the same linear memory. Streaming it out is what lowers them, and the window does exactly that, into the browser’s private storage.
Why every job runs on a worker
Threads in WebAssembly need the standard library rebuilt with atomics, which only nightly does. So cargo xtask web compiles on the nightly toolchain with -Z build-std=panic_abort,std and -C target-feature=+atomics,+bulk-memory,+mutable-globals, imports a shared memory with --max-memory=4294967296 (wasm32’s whole 4 GiB), and writes into target/web/, where none of those flags can rebuild the stable desktop builds. The release job pins one nightly by date, so the same tag builds the same module twice.
Shared memory also needs a cross-origin-isolated page, which means two headers: Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. A _headers file asks the host for them. Where the host will not send them, a service worker of ours adds both to every response of the origin and the page reloads once; where even that fails, the page prints the two header names and stops. There is no single-threaded fallback window.
And the page’s own thread may never block. The HTML Standard puts it in one line:
Only shared and dedicated worker agents allow the use of JavaScript Atomics APIs to potentially block.
On the page’s thread a contended lock, a blocking recv or a parallel loop waiting for help does not stall, it traps. Every job therefore runs on a worker, and the page’s thread joins a rayon pool of exactly one, built with use_current_thread, so a parallel loop it does run runs in place, in order, waiting for nobody. Progress comes back over channels the window drains with try_recv, as at the desk.
One browser rule finishes the model: Chromium starts a worker’s child only once that worker has returned to its event loop, which a worker building a thread pool never does. So only the page starts workers. A worker that needs another asks the page over postMessage, and the first job that needs rayon’s global pool builds it. Each worker brings the module up on the shared memory with a 2 MiB stack, takes one task off a Rust channel, runs it, and hands its stack and thread-locals back.
Atomics also make GPU objects not Send, so the viewport keeps its render resources in a thread-local of the page’s thread instead of egui’s shared callback map. Past that the window is one crate for both targets, with a cfg in the few places where a browser differs, which is why the tab shows the viewport and the panels of the desktop window rather than a reduced page.
Writing the file is the one place where the page’s rules cost real seconds. A worker writes the sliced file through a FileSystemSyncAccessHandle, the only synchronous file handle a browser offers, and one it hands to workers alone. One call at a time instead of a megabyte at a time turned 1.2 s into 36 s on a plate of about a million faces: same bytes out, thirty times the wall clock. The cure was a BufWriter with a megabyte of capacity.
What it weighs
The window’s module is 13 MB of .wasm, 3.7 MB under brotli, from the 2026-10-04 build; the pipeline on its own is 1.5 MB, 425 KB under brotli, from the 2026-10-03 run. The release job refuses a module over 26 214 400 bytes, Cloudflare’s 25 MiB per-file limit, which puts the ceiling at about twice the window as it stands.
wasm-opt -O3 and -Oz came out within 2 % of each other on the pipeline module, so both builds ask for speed and take whatever size that leaves.
The 4 GB address space of wasm32, the limit everyone expects to bite first, has not. The worst plate we measured, a hollowed bust writing a 275 MB file, peaked at 1138 MB of linear memory. What runs the pipeline build out of room is the finished file sitting in that memory, not the ceiling above it, which is why the window writes it to private storage a megabyte at a time.
What the tab cannot do
A browser reaches no printer. Discovery is a UDP broadcast, and the upload is a POST whose answer a page may read only if the board sends CORS headers. So the Network card in a tab says, in full:
A browser cannot reach a printer on your network. What is sliced downloads, and goes over on a stick or through the printer’s own app.
What downloads is the file the same writer produces at the desk (what is inside one). Three smaller things the tab does not do:
- Warn you before it closes. There is no prompt over unsaved work: closing the tab closes the window.
- Report a worker that panicked. The panic ends that worker with a line in the console and no outcome, so the job it was running never finishes. That is the failure to recognise when a slice in the tab stops reporting progress.
- Update itself. The Updates page is hidden, and a new build takes over only once every tab of the old one is closed: a running page’s threads have already imported the old script, and a new script over the old memory would not run.
Sources
- Encrust: the browser build · Both measured blocks, the commands that produce them, and what the page does not have yet
- Encrust: cargo xtask web · The nightly flags the threaded build needs, and where they are kept apart from the stable builds
- HTML Standard: agents and agent clusters · Which agents may block on Atomics, which is why no job runs on the page's own thread
- File System Standard: FileSystemSyncAccessHandle · The synchronous handle a worker writes the sliced file through, exposed to dedicated workers only