diff --git a/.agents/docs/binding-cases.md b/.agents/docs/binding-cases.md index 440dafc..40dd420 100644 --- a/.agents/docs/binding-cases.md +++ b/.agents/docs/binding-cases.md @@ -169,6 +169,20 @@ Rust converts the JavaScript object and all supplied collections into an owned p Taffy 0.13 exposes the stored value through `style(&self) -> &Style` and replaces it through `set_style(Style)`, with no public mutable or take operation. `std::mem::take` would move a `Vec` allocation rather than clone it if the binding had `&mut Style`, but that reference cannot be obtained safely from the high-level tree. The implemented fallback returns an empty patch before cloning; every nonempty patch clones the current Style once, then compares and applies supplied fields in one traversal. An unchanged candidate is discarded without dirtying, while a changed candidate is validated and written once. Replacing a collection can therefore clone the old collection before overwriting it, but there is no workload benchmark justifying the larger selective-reconstruction implementation in the initial API. This preserves Taffy's sole ownership and avoids a JavaScript shadow Style; a future upstream closure-style update operation could safely expose in-place mutation and automatic dirty propagation. +A focused end-to-end native-binding microbenchmark on implementation commit `6ed48b4` retained the JavaScript-to-Rust boundary, input conversion, mutation, and alternating real value changes. It ran on Node.js 24.19.0 on Linux with an Intel Core i5-13500H, pinned to one core, after warmup, with 13 samples in each of two runs. Representative per-operation timings were: + +| Preserved state and changed value | `setStyle` reconstruction | `updateStyle` | Observed relation | +| ------------------------------------------------------------ | ------------------------: | ------------: | ------------------------: | +| One retained scalar; replace one scalar | 3.83 µs | 3.89 µs | Effectively equal | +| 13 retained sparse fields; replace one scalar | 11.8 µs | 3.89 µs | `updateStyle` 3.0× faster | +| Complete 41-field `getStyle` snapshot; replace one scalar | 23.1 µs | 3.97 µs | `updateStyle` 5.8× faster | +| 1,000 retained `gridAutoRows`; replace one scalar | 428 µs | 4.60 µs | `updateStyle` 93× faster | +| Replace 1,000 `gridAutoRows` | 446 µs | 448 µs | Effectively equal | +| 1,000 retained nested string grid values; replace one scalar | 139 µs | 64.3 µs | `updateStyle` 2.2× faster | +| Replace 1,000 nested string grid values | 139 µs | 196 µs | `setStyle` 1.4× faster | + +These measurements support recommending `updateStyle` for incremental changes: it avoids reconverting retained JavaScript fields, and the advantage grows when retained input is larger. They do not establish per-call dominance. When the changed field itself is the large collection, both operations must parse its replacement and can converge; with string-heavy replacements, the current full-Style clone fallback can make `updateStyle` slower. This focused boundary microbenchmark also does not justify adding selective reconstruction without representative workload evidence. Public documentation should therefore say that `updateStyle` is generally faster for incremental changes, not that every call must be faster. + The pointer-backed calc variant is a stop rather than a routine field mapping. The public high-level Style vocabulary excludes calc because `TaffyTree` resolves every calc pointer to zero and exposes no resolver. Whether the Cargo feature is also disabled is an implementation choice to verify separately; keeping an internal feature enabled does not make calc a supported JavaScript value. ### Selected closed-enum representation diff --git a/.agents/docs/taffyjs-node-decisions.md b/.agents/docs/taffyjs-node-decisions.md index c4729d4..3207a78 100644 --- a/.agents/docs/taffyjs-node-decisions.md +++ b/.agents/docs/taffyjs-node-decisions.md @@ -198,6 +198,16 @@ New public state owners, compatibility layers, retained JavaScript values, callb **Source:** Yunfei (`@hyfdev`), 2026-08-16; kept `setStyle` as the direct complete-replacement operation, chose an additive update operation for ergonomic partial changes, required Rust-owned copying rather than JavaScript object cloning, required invalid, empty, and unchanged updates not to produce mutation or new dirty state, accepted whole-array replacement, and explicitly confirmed that generated TypeScript must prevent partial tagged-union inputs before asking for this accumulated design to be vouched. +### Preferred style mutation operation + +**Ruling:** Public documentation must recommend `updateStyle` as the default operation for changing an existing node. `setStyle` is the intentional complete-replacement operation and should be chosen when omitted fields must reset to Taffy's defaults, including a complete reset with `{}`. + +**Limits:** This recommendation does not remove or deprecate `setStyle`, change whole-value replacement for supplied arrays and complete values, promise that every individual `updateStyle` call is faster, or fix the binding's private copying strategy. The exact cost depends on the input shape and runtime. + +**Why:** Reconstructing preserved state for `setStyle` requires a caller-owned prior value or a `getStyle` snapshot and makes every retained supplied value cross the JavaScript-to-Rust conversion boundary again. `updateStyle` directly represents the incremental intent and generally avoids converting unrelated JavaScript values. Retained end-to-end measurements support this qualitative default while also showing that direct replacement of a large collection can make the operations converge, so the public claim must remain comparative rather than absolute. + +**Source:** Yunfei (`@hyfdev`), 2026-08-16; explicitly requested a dedicated `@taffyjs/node` documentation page that explains the distinction and tells readers to prefer `updateStyle` unless they have a specific need for `setStyle`. + ## Open ### Selective query implementation details diff --git a/.agents/docs/website.md b/.agents/docs/website.md index 39b60f5..cb9baaf 100644 --- a/.agents/docs/website.md +++ b/.agents/docs/website.md @@ -42,7 +42,7 @@ The target page groups are: - Guide: Introduction and Getting Started only. - Essentials: the tree-compute-read workflow, styles and values, Block, Flexbox, Grid, and measuring text and images. -- `@taffyjs/node`: package overview and supported targets; `TaffyTree` node and topology operations, styles and context, computation and dirty state, and layout results; the grouped `Style` data model; value helpers; and errors. +- `@taffyjs/node`: package overview and supported targets; `TaffyTree` node and topology operations, styles and context, the detailed `setStyle` versus `updateStyle` choice, computation and dirty state, and layout results; the grouped `Style` data model; value helpers; and errors. - `@taffyjs/wasm`: package overview and setup, supported environments, initialization and deployment, observable differences from Node, and a link to the shared direct API reference. - `@taffyjs/yoga`: package overview and installation, compatibility scope, migration from Yoga, Yoga-facing API reference, and documented differences from the direct Taffy API. diff --git a/apps/website/.vitepress/config.ts b/apps/website/.vitepress/config.ts index 5908a84..14379d3 100644 --- a/apps/website/.vitepress/config.ts +++ b/apps/website/.vitepress/config.ts @@ -25,6 +25,10 @@ const documentationSidebar = [ { text: "Overview", link: "/node/" }, { text: "Nodes and Topology", link: "/node/nodes-and-topology" }, { text: "Styles and Context", link: "/node/styles-and-context" }, + { + text: "setStyle vs updateStyle", + link: "/node/set-style-vs-update-style", + }, { text: "Computing Layout", link: "/node/computing-layout" }, { text: "Layout Results", link: "/node/layout-results" }, { text: "Style", link: "/node/style" }, diff --git a/apps/website/guide/styles-and-values.md b/apps/website/guide/styles-and-values.md index 8ce679d..688009e 100644 --- a/apps/website/guide/styles-and-values.md +++ b/apps/website/guide/styles-and-values.md @@ -67,6 +67,8 @@ Arrays, tagged values, and other complete records are replaced as complete value The prospective merged style is validated before it is stored. A failed update changes neither style nor dirty state. An empty update or one that supplies only already-stored values also leaves dirty state unchanged. +Unless you intentionally want a complete replacement, prefer `updateStyle` when changing an existing node. [setStyle vs updateStyle](../node/set-style-vs-update-style.md) explains the semantic and performance tradeoff in detail. + ## Named constants and tagged values Closed choices use frozen numeric families such as `Display`, `Overflow`, `FlexDirection`, and `AlignItems`: diff --git a/apps/website/node/set-style-vs-update-style.md b/apps/website/node/set-style-vs-update-style.md new file mode 100644 index 0000000..4dd2e53 --- /dev/null +++ b/apps/website/node/set-style-vs-update-style.md @@ -0,0 +1,89 @@ +# setStyle vs updateStyle + +When changing an existing node, prefer `updateStyle`. Use `setStyle` only when you intentionally want to replace the node's complete style or reset omitted fields to Taffy's defaults. + +Both methods accept ordinary partial-looking JavaScript objects, but omission has a different meaning: + +| Operation | `setStyle(node, input)` | `updateStyle(node, update)` | +| ------------------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------- | +| Omitted top-level field | Reset to its Taffy default | Preserve the stored value | +| Explicit `undefined` | Reset to its Taffy default | Preserve the stored value | +| Omitted component in `size`, `margin`, or another partial geometry record | Reset that component to its default | Preserve that component | +| Supplied array, tagged value, or complete record | Replace the complete value | Replace the complete value | +| Empty object | Reset the complete style | Make no change | +| Supplied values already match the stored values | Perform a complete replacement | Make no change and do not newly dirty the node | + +## Choose by intent + +Suppose a node already has a display mode, size, and flex growth: + +```ts +const node = tree.newLeaf({ + display: Display.Grid, + size: { width: 320, height: 180 }, + flexGrow: 1, +}); +``` + +`updateStyle` changes only what you name: + +```ts +tree.updateStyle(node, { flexGrow: 2 }); + +// display and size are preserved +``` + +The same object passed to `setStyle` means something different: + +```ts +tree.setStyle(node, { flexGrow: 2 }); + +// display and size are reset to their Taffy defaults +``` + +This makes `updateStyle` the direct expression of an incremental change. It also avoids keeping a complete JavaScript shadow of native style state merely so that unrelated values can be supplied again. + +## Why `updateStyle` is generally faster + +A `setStyle` replacement must convert every supplied field from JavaScript into its native Taffy representation. Rebuilding the replacement with object spread does not avoid that work: + +```ts +const nextStyle = { ...previousStyle, flexGrow: 2 }; +tree.setStyle(node, nextStyle); +``` + +The spread is shallow, but all retained fields in `nextStyle` still cross the JavaScript-to-native boundary again. Arrays and nested style values must be visited and converted even when they did not change. If `previousStyle` came from `getStyle`, producing that complete snapshot adds another native-to-JavaScript conversion first. + +`updateStyle` sends and converts only the supplied fields, then combines them with the stored style on the native side: + +```ts +tree.updateStyle(node, { flexGrow: 2 }); +``` + +The advantage usually grows with the number and complexity of retained fields. Small scalar-only styles may be close, while preserving large arrays or nested values can make reconstructing a `setStyle` input substantially more expensive. + +This is a default choice, not a promise that every individual `updateStyle` call is faster. Replacing a large collection still requires converting that collection, and exact costs depend on the values and runtime. The general rule remains: unless complete replacement is the intended behavior, prefer `updateStyle`. + +## When `setStyle` is the right operation + +Use `setStyle` when the input is the authoritative complete replacement and omission should mean “use the default.” Common examples are: + +- Resetting every field with `tree.setStyle(node, {})`. +- Applying a complete style snapshot or configuration that should replace earlier state. +- Deliberately clearing any old fields that the replacement does not name. + +Do not use `setStyle` plus `getStyle` or a caller-owned shadow object merely to update one field. That recreates incremental-update behavior in JavaScript and pays for converting the retained data again. + +## Updates are shallow by design + +`updateStyle` preserves omitted top-level fields and omitted components of partial `Point`, `Size`, `Rect`, and `Line` records. It does not recursively merge every nested value. A supplied array replaces the whole array, and a supplied tagged value or complete record replaces that whole value: + +```ts +tree.updateStyle(node, { + size: { width: 480 }, // preserves the current height + gridAutoRows: [], // clears every automatic row track + flexBasis: Dimension.Auto, // replaces the complete tagged value +}); +``` + +See [Styles and Values](../guide/styles-and-values.md) for the shared value rules and the [Style reference](./style.md) for the complete field groups. diff --git a/apps/website/node/styles-and-context.md b/apps/website/node/styles-and-context.md index e92c19d..c8dae27 100644 --- a/apps/website/node/styles-and-context.md +++ b/apps/website/node/styles-and-context.md @@ -35,6 +35,8 @@ Floating-point style values accept JavaScript numbers and are stored with Taffy' `getStyle(node)` returns a complete detached snapshot, including defaults. Its fields are recursively readonly in TypeScript, but its runtime objects are not frozen. Mutating a returned object does not change the tree. A style snapshot can be passed back as a later `StyleInput` or `StyleUpdate` because its structure is compatible. +For ordinary changes to an existing node, prefer `updateStyle`. Use `setStyle` when complete replacement or resetting omitted fields to their defaults is the intended operation. See [setStyle vs updateStyle](./set-style-vs-update-style.md) for the detailed comparison and performance model. + ## Store JavaScript context The generic parameter on `TaffyTree` describes the values returned by `getNodeContext` and supplied to measurement: