Benchmarks
These measurements describe the current implementations, not their final performance potential; several known optimization opportunities have not yet been implemented.
Each scenario names a transaction and measures every package that can express it. +2.0× is twice as fast as that scenario's reference, -2.0× takes twice as long; a dash means the package has no way to write the workload. Every number comes from one machine in one run, so compare the ratios rather than the milliseconds.
Each package runs its layout engine either as native code or as WebAssembly:
yoga-layout: WebAssembly (wasm32-unknown-emscripten)@taffyjs/node: native code@taffyjs/wasm: WebAssembly (wasm32-wasip1)@taffyjs/yoga: native code@taffyjs/yoga-wasm: WebAssembly (wasm32-wasip1)
yoga-layout | @taffyjs/node | @taffyjs/wasm | @taffyjs/yoga | @taffyjs/yoga-wasm | |
|---|---|---|---|---|---|
| Incremental frameone text node | 1.00×0.436 ms | +1.9×0.228 ms | -46×20.2 ms | -4.2×1.81 ms | -61×26.4 ms |
| Incremental framea tenth of the text nodes | 1.00×0.588 ms | -6.3×3.73 ms | -47×27.7 ms | -3.7×2.16 ms | -46×27.0 ms |
| Incremental frameevery text node | 1.00×2.55 ms | -1.9×4.73 ms | -12×30.1 ms | -2.0×5.03 ms | -14×36.1 ms |
| One-shot rendersmall screen | 1.00×0.162 ms | +1.4×0.115 ms | -13×2.14 ms | -3.6×0.588 ms | -23×3.74 ms |
| One-shot rendermedium screen | 1.00×1.38 ms | +1.1×1.24 ms | -7.6×10.5 ms | -3.3×4.48 ms | -20×27.4 ms |
| One-shot renderlarge screen | 1.00×5.69 ms | +1.0×5.56 ms | -6.9×39.4 ms | -3.4×19.2 ms | -18×104 ms |
| Measured rendermedium screen | 1.00×1.40 ms | -1.1×1.58 ms | -9.5×13.4 ms | -3.3×4.64 ms | -20×28.2 ms |
| Nesting depth2 wrapper levels | 1.00×0.799 ms | +1.7×0.479 ms | -8.6×6.89 ms | -2.8×2.23 ms | -19×14.9 ms |
| Nesting depth6 wrapper levels | 1.00×0.737 ms | +1.4×0.528 ms | -9.2×6.82 ms | -2.8×2.09 ms | -18×13.5 ms |
| Nesting depth10 wrapper levels | 1.00×0.741 ms | +1.4×0.544 ms | -9.1×6.74 ms | -2.7×2.00 ms | -17×13.0 ms |
| DashboardCSS Grid | — | 1.00×0.412 ms | -16×6.50 ms | — | — |
| Dashboardnested flex | 1.00×0.606 ms | +1.2×0.503 ms | -11×6.87 ms | -5.4×3.28 ms | -35×21.4 ms |
| Cold start to first layout | 1.00×22.0 ms | +4.1×5.31 ms | -1.6×36.3 ms | +1.9×11.9 ms | -2.1×46.7 ms |
Incremental frameone text node
An application screen stays in memory between frames. Each transaction dirties part of it, recomputes, and reads every box, the way a terminal UI or any persistent interface does on every commit.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +1.9× | 0.228 ms | 0.286 ms | 5 | 4,656 | ±0.14% | 21237 |
yoga-layout | 1.00× | 0.436 ms | 0.485 ms | 1 | 4,656 | ±0.09% | 11415 |
@taffyjs/yoga | -4.2× | 1.81 ms | 2.84 ms | 3 | 4,656 | ±0.79% | 2618 |
@taffyjs/wasm | -46× | 20.2 ms | 20.5 ms | 5 | 4,656 | ±0.15% | 249 |
@taffyjs/yoga-wasm | -61× | 26.4 ms | 31.5 ms | 3 | 4,656 | ±0.76% | 189 |
Incremental framea tenth of the text nodes
An application screen stays in memory between frames. Each transaction dirties part of it, recomputes, and reads every box, the way a terminal UI or any persistent interface does on every commit.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
yoga-layout | 1.00× | 0.588 ms | 0.650 ms | 10 | 4,656 | ±0.08% | 8461 |
@taffyjs/yoga | -3.7× | 2.16 ms | 3.22 ms | 53 | 4,656 | ±0.80% | 2208 |
@taffyjs/node | -6.3× | 3.73 ms | 4.92 ms | 785 | 4,656 | ±0.53% | 1327 |
@taffyjs/yoga-wasm | -46× | 27.0 ms | 29.9 ms | 53 | 4,656 | ±0.53% | 186 |
@taffyjs/wasm | -47× | 27.7 ms | 31.1 ms | 785 | 4,656 | ±0.76% | 180 |
Incremental frameevery text node
An application screen stays in memory between frames. Each transaction dirties part of it, recomputes, and reads every box, the way a terminal UI or any persistent interface does on every commit.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
yoga-layout | 1.00× | 2.55 ms | 2.71 ms | 87 | 4,656 | ±0.09% | 1951 |
@taffyjs/node | -1.9× | 4.73 ms | 5.57 ms | 1,074 | 4,656 | ±0.47% | 1047 |
@taffyjs/yoga | -2.0× | 5.03 ms | 6.71 ms | 739 | 4,656 | ±0.97% | 963 |
@taffyjs/wasm | -12× | 30.1 ms | 33.1 ms | 1,074 | 4,656 | ±0.52% | 166 |
@taffyjs/yoga-wasm | -14× | 36.1 ms | 39.6 ms | 739 | 4,656 | ±0.71% | 138 |
One-shot rendersmall screen
Every text size is already known, so the transaction is node creation, style conversion, one layout pass, a complete box-model read, and teardown. This is the shape a document or image renderer repeats per page.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +1.4× | 0.115 ms | 0.182 ms | 0 | 592 | ±0.21% | 42318 |
yoga-layout | 1.00× | 0.162 ms | 0.221 ms | 0 | 592 | ±0.11% | 30406 |
@taffyjs/yoga | -3.6× | 0.588 ms | 2.23 ms | 0 | 592 | ±2.11% | 7678 |
@taffyjs/wasm | -13× | 2.14 ms | 3.22 ms | 0 | 592 | ±1.05% | 2333 |
@taffyjs/yoga-wasm | -23× | 3.74 ms | 4.70 ms | 0 | 592 | ±0.58% | 1303 |
One-shot rendermedium screen
Every text size is already known, so the transaction is node creation, style conversion, one layout pass, a complete box-model read, and teardown. This is the shape a document or image renderer repeats per page.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +1.1× | 1.24 ms | 1.39 ms | 0 | 4,640 | ±0.15% | 3998 |
yoga-layout | 1.00× | 1.38 ms | 1.54 ms | 0 | 4,640 | ±0.15% | 3596 |
@taffyjs/yoga | -3.3× | 4.48 ms | 6.97 ms | 0 | 4,640 | ±2.69% | 1012 |
@taffyjs/wasm | -7.6× | 10.5 ms | 13.2 ms | 0 | 4,640 | ±1.16% | 469 |
@taffyjs/yoga-wasm | -20× | 27.4 ms | 32.3 ms | 0 | 4,640 | ±0.62% | 184 |
One-shot renderlarge screen
Every text size is already known, so the transaction is node creation, style conversion, one layout pass, a complete box-model read, and teardown. This is the shape a document or image renderer repeats per page.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +1.0× | 5.56 ms | 5.93 ms | 0 | 18,624 | ±0.29% | 897 |
yoga-layout | 1.00× | 5.69 ms | 6.10 ms | 0 | 18,624 | ±0.17% | 874 |
@taffyjs/yoga | -3.4× | 19.2 ms | 35.1 ms | 0 | 18,624 | ±2.59% | 256 |
@taffyjs/wasm | -6.9× | 39.4 ms | 42.3 ms | 0 | 18,624 | ±0.57% | 128 |
@taffyjs/yoga-wasm | -18× | 104 ms | 111 ms | 0 | 18,624 | ±0.68% | 50 |
Measured rendermedium screen
The medium one-shot render tree with its text sized through the public measure callback instead of fixed sizes. Node count and structure are unchanged, but measured text wraps, so this scenario carries both the callback crossings and the layout work that wrapping creates.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
yoga-layout | 1.00× | 1.40 ms | 1.57 ms | 37 | 4,640 | ±0.14% | 3522 |
@taffyjs/node | -1.1× | 1.58 ms | 1.98 ms | 440 | 4,640 | ±0.52% | 3105 |
@taffyjs/yoga | -3.3× | 4.64 ms | 7.77 ms | 301 | 4,640 | ±2.64% | 981 |
@taffyjs/wasm | -9.5× | 13.4 ms | 14.9 ms | 440 | 4,640 | ±0.48% | 372 |
@taffyjs/yoga-wasm | -20× | 28.2 ms | 32.6 ms | 301 | 4,640 | ±0.82% | 177 |
Nesting depth2 wrapper levels
One node budget, rearranged. Each row of content is wrapped in a chain of column containers whose width comes from the parent rather than an explicit value, which is what writing a column inside a column produces.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +1.7× | 0.479 ms | 0.600 ms | 0 | 808 | ±0.34% | 10232 |
yoga-layout | 1.00× | 0.799 ms | 0.903 ms | 0 | 808 | ±0.11% | 6197 |
@taffyjs/yoga | -2.8× | 2.23 ms | 4.82 ms | 0 | 808 | ±2.63% | 2003 |
@taffyjs/wasm | -8.6× | 6.89 ms | 7.86 ms | 0 | 808 | ±0.37% | 722 |
@taffyjs/yoga-wasm | -19× | 14.9 ms | 18.8 ms | 0 | 808 | ±1.12% | 332 |
Nesting depth6 wrapper levels
One node budget, rearranged. Each row of content is wrapped in a chain of column containers whose width comes from the parent rather than an explicit value, which is what writing a column inside a column produces.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +1.4× | 0.528 ms | 0.628 ms | 0 | 800 | ±0.23% | 9331 |
yoga-layout | 1.00× | 0.737 ms | 0.839 ms | 0 | 800 | ±0.11% | 6711 |
@taffyjs/yoga | -2.8× | 2.09 ms | 4.52 ms | 0 | 800 | ±2.64% | 2132 |
@taffyjs/wasm | -9.2× | 6.82 ms | 7.51 ms | 0 | 800 | ±0.33% | 731 |
@taffyjs/yoga-wasm | -18× | 13.5 ms | 15.6 ms | 0 | 800 | ±0.60% | 365 |
Nesting depth10 wrapper levels
One node budget, rearranged. Each row of content is wrapped in a chain of column containers whose width comes from the parent rather than an explicit value, which is what writing a column inside a column produces.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +1.4× | 0.544 ms | 0.655 ms | 0 | 788 | ±0.26% | 9035 |
yoga-layout | 1.00× | 0.741 ms | 0.844 ms | 0 | 788 | ±0.11% | 6682 |
@taffyjs/yoga | -2.7× | 2.00 ms | 4.50 ms | 0 | 788 | ±2.87% | 2227 |
@taffyjs/wasm | -9.1× | 6.74 ms | 7.65 ms | 0 | 788 | ±0.33% | 739 |
@taffyjs/yoga-wasm | -17× | 13.0 ms | 16.0 ms | 0 | 788 | ±0.79% | 378 |
DashboardCSS Grid
Twenty-four cards placed by column span in one Grid container. Yoga has no Grid, so this scenario runs only the packages that do and takes @taffyjs/node as its reference.
vs @taffyjs/node | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | 1.00× | 0.412 ms | 0.503 ms | 0 | 772 | ±0.13% | 11915 |
@taffyjs/wasm | -16× | 6.50 ms | 7.31 ms | 0 | 772 | ±0.31% | 766 |
Dashboardnested flex
The same twenty-four cards and the same picture, built the way a Yoga user has to build it: rows of percentage-width columns. It needs its own row containers, which is why its node count is higher.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +1.2× | 0.503 ms | 0.592 ms | 0 | 808 | ±0.12% | 9808 |
yoga-layout | 1.00× | 0.606 ms | 0.704 ms | 0 | 808 | ±0.13% | 8139 |
@taffyjs/yoga | -5.4× | 3.28 ms | 6.65 ms | 0 | 808 | ±2.14% | 1395 |
@taffyjs/wasm | -11× | 6.87 ms | 7.92 ms | 0 | 808 | ±0.46% | 723 |
@taffyjs/yoga-wasm | -35× | 21.4 ms | 23.7 ms | 0 | 808 | ±0.59% | 235 |
Cold start to first layout
A new Node process imports the package, builds a small screen, lays it out once, reads every box, and exits. Nothing is warmed and nothing is amortised, so module load and runtime instantiation are inside the number.
vs yoga-layout | Median | p99 | Measure calls | Values read | ± RME | Samples | |
|---|---|---|---|---|---|---|---|
@taffyjs/node | +4.1× | 5.31 ms | 5.85 ms | 59 | 148 | ±1.36% | 26 |
@taffyjs/yoga | +1.9× | 11.9 ms | 12.5 ms | 39 | 148 | ±0.74% | 26 |
yoga-layout | 1.00× | 22.0 ms | 22.9 ms | 5 | 148 | ±1.13% | 26 |
@taffyjs/wasm | -1.6× | 36.3 ms | 37.5 ms | 59 | 148 | ±0.69% | 26 |
@taffyjs/yoga-wasm | -2.1× | 46.7 ms | 54.1 ms | 39 | 148 | ±1.31% | 26 |
CPU Apple M3 ProOS macOS (Darwin 25.6.0, arm64)Node.js v24.20.0Commit 93c4ba79c1