You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@taffyjs/node matches or beats yoga-layout on every benchmark scenario.
The Taffy 0.14.0 update in #57 puts nine of the twelve comparable scenarios at parity or ahead. The three that remain all size text through JavaScript callbacks. This issue tracks the four remaining package-side items after the upstream cache fix.
Where the package stands
The table comes from the complete publication profile in #57, measured with Node.js v24.19.0 on the fixed Apple M3 Pro host. + means @taffyjs/node is faster and - means it is slower. Run-to-run variation makes results inside 1.10× parity rather than a win.
Scenario
Result
Median, node / Yoga
Measure callbacks, node / Yoga
one-shot-render-small
+1.39×
0.116 / 0.161 ms
0 / 0
one-shot-render-medium
+1.10×
1.232 / 1.360 ms
0 / 0
one-shot-render-large
+1.03×
5.632 / 5.808 ms
0 / 0
measured-render-medium
-1.13×
1.610 / 1.425 ms
440 / 37
nesting-depth-2
+1.69×
0.484 / 0.818 ms
0 / 0
nesting-depth-6
+1.37×
0.539 / 0.742 ms
0 / 0
nesting-depth-10
+1.31×
0.575 / 0.752 ms
0 / 0
incremental-frame-one-leaf
+1.86×
0.235 / 0.436 ms
5 / 1
incremental-frame-one-tenth
-6.39×
3.771 / 0.591 ms
785 / 10
incremental-frame-every-leaf
-1.87×
4.798 / 2.571 ms
1,074 / 87
dashboard-flex-emulation
+1.19×
0.526 / 0.625 ms
0 / 0
dashboard-grid
0.440 ms, no Yoga equivalent
0.440 ms
0 / —
cold-start
+4.13×
5.909 / 24.403 ms
59 / 5
All benchmark rounds passed the existing 10% relative-margin-of-error and 25% round-median-spread gates.
Items
1. Keep measurement requests with different parent sizes from evicting each other. Filed upstream as DioxusLabs/taffy#1148, fixed by DioxusLabs/taffy#1010, and shipped in Taffy 0.14.0.
The old cache selected one of nine fixed entries without considering the parent size, then required the parent width to match when reading that entry. Two requests for an auto-width flex column therefore selected the same entry, evicted each other, and repeated the subtree work at every nesting level.
Taffy 0.14.0 keeps nine entries per node and uses second-chance eviction, so requests with different parent sizes can coexist. The current publication run takes 0.484 / 0.539 / 0.575 ms at nesting depths 2 / 6 / 10, and the one-leaf incremental scenario reaches the measure callback 5 times.
The same Taffy update also fixes DioxusLabs/taffy#1149: flexbox now measures content size consistently and applies the item's preferred style size itself. fix: update Taffy to 0.14.0 #57 adds the three-node public API regression to the suite shared by native and Wasm.
2. Taffy asks measured leaves for their size far more often than Yoga, cold and incremental alike. Two behaviors remain visible in the callback counts:
Repeated requests for the same leaf.measured-render-medium is a cold one-shot render with 37 text nodes. Taffy calls their callbacks 440 times while Yoga calls 37. incremental-frame-every-leaf is 1,074 against 87, the same approximate ratio.
Requests for leaves nothing touched.incremental-frame-one-tenth dirties about nine of the 87 text nodes. Yoga measures ten; Taffy measures 785 across the tree.
These are the only three scenarios outside parity after item 1: measured render takes 1.610 ms against Yoga's 1.425 ms, the one-tenth frame takes 3.771 ms against 0.591 ms, and the every-leaf frame takes 4.798 ms against 2.571 ms. Earlier analysis in Reduce redundant Taffy measurement passes for measured trees #43 attributed about half of a comparable count to Taffy's automatic minimum size on intermediate containers.
3. Each measure callback costs 0.71–0.85 µs to cross the binding. A call that crosses and returns a boolean costs 0.19 µs, so most of the difference is encoding the constraint record and reading two properties off the returned JavaScript object. At 1,074 callbacks that is 0.86 ms, about 18% of the current 4.798 ms incremental-frame-every-leaf transaction. Returning the size through the same buffer that carries the constraints would remove the property reads. Related: Avoid rebuilding the full Style for every measure callback #39, which removed the per-callback style rebuild; the measure path still hands out a complete style snapshot through a per-node provider in crates/taffyjs_binding/src/measure.rs.
4. A released tree does not return its memory, and V8 is never told the memory exists. Building a 1,000-node tree per round, forcing a collection before each sample, resident memory goes 330 MB → 1,559 MB over 1,200 rounds. Calling clear() makes no difference (1,557 MB without it), and external stays flat throughout. crates/taffyjs_binding never calls napi_adjust_external_memory, so V8 has no reason to collect the wrapper that owns the native tree, and the public API has no way to release a tree — clear() only empties it. Report the allocation to V8 and add an explicit release method, with Symbol.dispose alongside it.
5. Reading layout costs one binding call per node. Tracked in Add a compact bulk layout read path #41. Each getLayout costs 0.22 µs, of which 0.19 µs is the crossing itself — the same as an isDirty call that carries no buffer — and 0.03 µs the codec. Over the 1,164-node tree that is 0.288 ms for the four values per node the incremental scenarios read, and 0.30 ms for the sixteen the one-shot scenarios read. Pulling the larger pattern back in one call would leave 26 µs of JavaScript to walk the resulting Float64Array.
This is not a parity item. yoga-layout needs 0.362 ms and 1.57 ms for those same two reads, so the package is already ahead on both. The read is about 7.6% of the one-tenth incremental frame and 6.0% of the every-leaf frame. The current one-shot medium and large results are +1.10× and +1.03×; reducing their read overhead would create more margin without changing layout work.
Reproducing
Run the complete publication profile with vp run benchmark:update-website from the repository root. A single local measurement is node --expose-gc worker.ts <targetId> <scenarioId> local from benchmarks/; its samplesMs array is sorted and shows the distribution rather than sample order. The native and Wasm conformance suites are vp run check and vp run check:wasm.
Goal
@taffyjs/nodematches or beatsyoga-layouton every benchmark scenario.The Taffy 0.14.0 update in #57 puts nine of the twelve comparable scenarios at parity or ahead. The three that remain all size text through JavaScript callbacks. This issue tracks the four remaining package-side items after the upstream cache fix.
Where the package stands
The table comes from the complete
publicationprofile in #57, measured with Node.js v24.19.0 on the fixed Apple M3 Pro host.+means@taffyjs/nodeis faster and-means it is slower. Run-to-run variation makes results inside 1.10× parity rather than a win.one-shot-render-smallone-shot-render-mediumone-shot-render-largemeasured-render-mediumnesting-depth-2nesting-depth-6nesting-depth-10incremental-frame-one-leafincremental-frame-one-tenthincremental-frame-every-leafdashboard-flex-emulationdashboard-gridcold-startAll benchmark rounds passed the existing 10% relative-margin-of-error and 25% round-median-spread gates.
Items
1. Keep measurement requests with different parent sizes from evicting each other. Filed upstream as DioxusLabs/taffy#1148, fixed by DioxusLabs/taffy#1010, and shipped in Taffy 0.14.0.
The old cache selected one of nine fixed entries without considering the parent size, then required the parent width to match when reading that entry. Two requests for an auto-width flex column therefore selected the same entry, evicted each other, and repeated the subtree work at every nesting level.
Taffy 0.14.0 keeps nine entries per node and uses second-chance eviction, so requests with different parent sizes can coexist. The current publication run takes 0.484 / 0.539 / 0.575 ms at nesting depths 2 / 6 / 10, and the one-leaf incremental scenario reaches the measure callback 5 times.
The same Taffy update also fixes DioxusLabs/taffy#1149: flexbox now measures content size consistently and applies the item's preferred style size itself. fix: update Taffy to 0.14.0 #57 adds the three-node public API regression to the suite shared by native and Wasm.
2. Taffy asks measured leaves for their size far more often than Yoga, cold and incremental alike. Two behaviors remain visible in the callback counts:
Repeated requests for the same leaf.
measured-render-mediumis a cold one-shot render with 37 text nodes. Taffy calls their callbacks 440 times while Yoga calls 37.incremental-frame-every-leafis 1,074 against 87, the same approximate ratio.Requests for leaves nothing touched.
incremental-frame-one-tenthdirties about nine of the 87 text nodes. Yoga measures ten; Taffy measures 785 across the tree.These are the only three scenarios outside parity after item 1: measured render takes 1.610 ms against Yoga's 1.425 ms, the one-tenth frame takes 3.771 ms against 0.591 ms, and the every-leaf frame takes 4.798 ms against 2.571 ms. Earlier analysis in Reduce redundant Taffy measurement passes for measured trees #43 attributed about half of a comparable count to Taffy's automatic minimum size on intermediate containers.
3. Each measure callback costs 0.71–0.85 µs to cross the binding. A call that crosses and returns a boolean costs 0.19 µs, so most of the difference is encoding the constraint record and reading two properties off the returned JavaScript object. At 1,074 callbacks that is 0.86 ms, about 18% of the current 4.798 ms
incremental-frame-every-leaftransaction. Returning the size through the same buffer that carries the constraints would remove the property reads. Related: Avoid rebuilding the full Style for every measure callback #39, which removed the per-callback style rebuild; the measure path still hands out a complete style snapshot through a per-node provider incrates/taffyjs_binding/src/measure.rs.4. A released tree does not return its memory, and V8 is never told the memory exists. Building a 1,000-node tree per round, forcing a collection before each sample, resident memory goes 330 MB → 1,559 MB over 1,200 rounds. Calling
clear()makes no difference (1,557 MB without it), andexternalstays flat throughout.crates/taffyjs_bindingnever callsnapi_adjust_external_memory, so V8 has no reason to collect the wrapper that owns the native tree, and the public API has no way to release a tree —clear()only empties it. Report the allocation to V8 and add an explicit release method, withSymbol.disposealongside it.5. Reading layout costs one binding call per node. Tracked in Add a compact bulk layout read path #41. Each
getLayoutcosts 0.22 µs, of which 0.19 µs is the crossing itself — the same as anisDirtycall that carries no buffer — and 0.03 µs the codec. Over the 1,164-node tree that is 0.288 ms for the four values per node the incremental scenarios read, and 0.30 ms for the sixteen the one-shot scenarios read. Pulling the larger pattern back in one call would leave 26 µs of JavaScript to walk the resultingFloat64Array.This is not a parity item.
yoga-layoutneeds 0.362 ms and 1.57 ms for those same two reads, so the package is already ahead on both. The read is about 7.6% of the one-tenth incremental frame and 6.0% of the every-leaf frame. The current one-shot medium and large results are +1.10× and +1.03×; reducing their read overhead would create more margin without changing layout work.Reproducing
Run the complete publication profile with
vp run benchmark:update-websitefrom the repository root. A single local measurement isnode --expose-gc worker.ts <targetId> <scenarioId> localfrombenchmarks/; itssamplesMsarray is sorted and shows the distribution rather than sample order. The native and Wasm conformance suites arevp run checkandvp run check:wasm.