You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/23 05:38:36 UTC

(maka) branch rust-preview updated: perf(nsh): sync bounded layout updates from upstream

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/rust-preview by this push:
     new c8e2757fca perf(nsh): sync bounded layout updates from upstream
c8e2757fca is described below

commit c8e2757fcae7c1d78942b020e6cb49f599521ffa
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Fri Oct 23 14:38:08 2026 +0900

    perf(nsh): sync bounded layout updates from upstream
    
    Integrate longbridge/nsh 111b3a933319fadb0f1da9a59909ca526f256386 by vendor-directory three-way merge. Preserve local terminal, reading, and hint behavior; allocation lifetimes keep stale hint observations from targeting reused layout slots.
    
    Document the 0.3.0 compatibility baseline and imported source provenance. Refresh only NSH notice inputs without changing license texts or unrelated metadata.
    
    Generated-by: OpenAI Codex
---
 products/maka/cli/notices.json                     |  25 +-
 third-party/nsh/AGENTS.md                          |   6 +-
 third-party/nsh/Cargo.toml                         |   2 +-
 third-party/nsh/SOURCE.md                          |   8 +-
 .../nsh/crates/nsh-core/src/core/classes.rs        |   3 +-
 .../nsh/crates/nsh-core/src/core/layout_tree.rs    | 560 +++++++++++++++++++++
 third-party/nsh/crates/nsh-core/src/core/mod.rs    | 209 ++++----
 .../nsh/crates/nsh-core/src/core/overlay.rs        |  42 +-
 .../crates/nsh-core/src/core/parent_viewport.rs    |  14 +-
 third-party/nsh/crates/nsh-core/src/core/tests.rs  | 173 ++++++-
 third-party/nsh/docs/dev/compatibility.md          |   3 +-
 third-party/nsh/docs/dev/performance.md            |   9 +
 third-party/nsh/docs/dev/pitfalls.md               |  17 +-
 third-party/nsh/upstream.json                      |  61 ++-
 14 files changed, 962 insertions(+), 170 deletions(-)

diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index 3780dfc726..1a34429107 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -1018,14 +1018,14 @@
     "third-party/llrt-crypto/src/subtle/verify.rs": "50b94c3f7476085fa15dfee704c1ae86b45746a715ce31fee0a8a3568562e3a5",
     "third-party/llrt-crypto/src/subtle/wrapping.rs": "438a0b83ff2309e3c040bb3d4d875b8bdd847f338455323427eeb1d1b07b2db6",
     "third-party/llrt-crypto/upstream.json": "eaf8f0a0b7b6027ea1656c21bf021885168681479b03ef925a622dfa788aa7bf",
-    "third-party/nsh/AGENTS.md": "6b2e0883676c03c9a021a67c517da2e098e67a879123bb7cd2da8c39d0fca4eb",
+    "third-party/nsh/AGENTS.md": "6670fd3b737cba0e6a8afe4fa5cab194a88192dcb41a00a67fd1753893bcbd88",
     "third-party/nsh/CLAUDE.md": "336cc4fbf19beaada7ccf9986414fa91851a8d7a07dfb3ccbe800a69eed0ab49",
     "third-party/nsh/CONTRIBUTING.md": "fb27100fec01ba22dde6da1546f9cd761b8e23bec7d47eca87146c7991e85de2",
     "third-party/nsh/Cargo.lock": "f1765287f4b60baad0fe758919f878f9da61b6ac0f3e8a17ed9488b3608955c6",
-    "third-party/nsh/Cargo.toml": "fc0059d9067e1ce8c9bebb563d4e244985346c557e0c9d121e16e807be4ce9c5",
+    "third-party/nsh/Cargo.toml": "8b8808f82642ac958bb922917b2d986b485c12fcc7667997ca2123c83ec06a92",
     "third-party/nsh/LICENSE-APACHE": "ec75427ad955e3c740f22e7dc3f6db6fc5b2cc1d9135c2af91cece8dd47a2d38",
     "third-party/nsh/README.md": "ff632ba97e2af5f8f0d69ed03250dd2a7d90f53da48b7a07976394d37f85aacb",
-    "third-party/nsh/SOURCE.md": "7fc4ab23e012ba163abfd441e66eea227bef1bb3135f96896eff2e5235220bdd",
+    "third-party/nsh/SOURCE.md": "443dbf9c5526010aa6bc3e94cbfc82970fbb7cbb90317b954c6508c76c945435",
     "third-party/nsh/crates/gpui/Cargo.lock": "2b8a4fd17ecabf2cb11520392322bf5aaa2736a8f79989d9e55ab8795360487b",
     "third-party/nsh/crates/gpui/Cargo.toml": "1d53eec1242cd480700a9e2ffd5c013f21c8b0c3207cb9544733fd5ea375f0f1",
     "third-party/nsh/crates/gpui/nsh-gpui-host/Cargo.toml": "f36b982a17b38bd1d91760a2d8d9db06b42db949c227878cef294a5ff2100844",
@@ -1213,18 +1213,19 @@
     "third-party/nsh/crates/nsh-core/src/canvas/hit.rs": "2e3528e7f911e5d4ab735b19e8f79a54a037748598edc88dd9309195106b8e36",
     "third-party/nsh/crates/nsh-core/src/clipboard.rs": "36b1c87f8762288ba9f8282daa03a5deb5541769afe75d58a7ba0cf8fe7defb1",
     "third-party/nsh/crates/nsh-core/src/color.rs": "93afe9bf38e8f7b12108fedc4863f8e1bee3a55ae7d0c75e788b79b02fb72a93",
-    "third-party/nsh/crates/nsh-core/src/core/classes.rs": "242ff14438c0785b6d29af6315273d4f0256cc048fad221b4cabd2f3a0f2027f",
+    "third-party/nsh/crates/nsh-core/src/core/classes.rs": "eeea690ba30e01bd1b74aebb83358475a62265fe2d902b119cb97389b84d8765",
     "third-party/nsh/crates/nsh-core/src/core/components.rs": "93f3cb23f3a570c620d8204bf46a125885f35cebe71e56e637eaafcb673dd057",
+    "third-party/nsh/crates/nsh-core/src/core/layout_tree.rs": "d52a8d38c0dd4d93d193b61d9070a8d969d3067e1212e1fc6af758d1446809e3",
     "third-party/nsh/crates/nsh-core/src/core/list.rs": "e709dcf029ac3d8d0c4bc87b4e1984ce07f50e18266d0759eaed773de1902684",
     "third-party/nsh/crates/nsh-core/src/core/measure.rs": "c6692abd3002576afef5b6d777d7694e28e4754bf7ada0adf8ed1c92419289c1",
-    "third-party/nsh/crates/nsh-core/src/core/mod.rs": "0dddd5ad69f1c2bf6e67fe69b0d49f8d8487e9c0b31c4f72d486cbab1e907406",
-    "third-party/nsh/crates/nsh-core/src/core/overlay.rs": "21be85fdbba3484b0e5ea2d7455f821c197bf3af195ef2532a5bdc37080987a5",
-    "third-party/nsh/crates/nsh-core/src/core/parent_viewport.rs": "7aba431169b7dbd3000896a2aa6af865b18286826ce031182c0903278ffb71b6",
+    "third-party/nsh/crates/nsh-core/src/core/mod.rs": "4c3df633d6260d6da543b675b23f46007c058e0eb078a2ee22b8e90ada2c7ba6",
+    "third-party/nsh/crates/nsh-core/src/core/overlay.rs": "4a65103b3cf7a35e11036532866b171dafd0bddbfe6517849520010e39d01239",
+    "third-party/nsh/crates/nsh-core/src/core/parent_viewport.rs": "af5798fb792c837a9cbd0654bb04aeb867ecf784d140ea159ebce326cbaf6e7d",
     "third-party/nsh/crates/nsh-core/src/core/reading.rs": "3bb3d25dc8b97f35eb73dcbaf2d6f1b5dff8cdbdd012a8edcef7fab6661f3d9f",
     "third-party/nsh/crates/nsh-core/src/core/renders.rs": "eba08379b95272762630258bba8c9d1908f8e8ec94d9518bfd59470edf49b5cd",
     "third-party/nsh/crates/nsh-core/src/core/services.rs": "8812eefd00661841ff2b1de5d4f8cf68dd667da4151b4c79ed966cc940934752",
     "third-party/nsh/crates/nsh-core/src/core/sticky.rs": "2c508038c1677fc10f11b5e6c1bf245cb78a75a02beaac24d0b09b2033a4beaa",
-    "third-party/nsh/crates/nsh-core/src/core/tests.rs": "1e4f1112be94a7cb3d86fa8242ef4a679b9e4016ee30a7ae4e4bddbb44ebc7fb",
+    "third-party/nsh/crates/nsh-core/src/core/tests.rs": "183cc85ffbedd4144a888af749433e1bbe77184548625c80fa0abeaef0877700",
     "third-party/nsh/crates/nsh-core/src/core/touch.rs": "a8559c023a443a6defad135af2f319e60130e7df23af2a8353239b0862e494a8",
     "third-party/nsh/crates/nsh-core/src/core/views.rs": "c32c59533d5ee33bc16677b0459816f7670c224f3f8de3507c72029989660834",
     "third-party/nsh/crates/nsh-core/src/css.rs": "56760520c706f64505849d5a32a1eada2d9fd243a5c70e07f21bc8ed0105217f",
@@ -1829,7 +1830,7 @@
     "third-party/nsh/docs/dev/api-naming.md": "5286d04f523bd17c9ceaecb37612a145c52a91684889293b43fbf6f0da7a9363",
     "third-party/nsh/docs/dev/architecture.md": "dd96d2a3ea5d26d23d17e0db36a1b280486be933eb83452f8fbb5d4c87023951",
     "third-party/nsh/docs/dev/canvas.md": "e6c644b1198801f9242aac91339dc04b5c480d876dbcbe378c58b4953f3970c2",
-    "third-party/nsh/docs/dev/compatibility.md": "0307638db496f5ea70fd6c996ae5839336c54e370ed495b8fb06d860eb6a02bd",
+    "third-party/nsh/docs/dev/compatibility.md": "26336d6349cbedd0a70990dd5a4d2e7e48c604242974ac164e68bbeb4ad1cddd",
     "third-party/nsh/docs/dev/components.md": "9f4a41c405a016275002fc9d850a222dcf941eb47f96201a1a384ba3f8a34914",
     "third-party/nsh/docs/dev/dock.md": "51527992aeec0387fe7ebdb82b7d24ed4ab6f0dc19b3332e23236cb7a3ad1041",
     "third-party/nsh/docs/dev/elements.md": "94da584a4b2a7dfbd54c83799329ed05220ce8f2c86107df1e556148dd2b790f",
@@ -1843,8 +1844,8 @@
     "third-party/nsh/docs/dev/linux.md": "4441ec64b6edc5c03084bd203aa5114c066f9e2c0541ee320be8f17b6f82c3fe",
     "third-party/nsh/docs/dev/macos.md": "827c809b5774681c77daf05ac6603342c027dfd459e78f55558044861711dcd3",
     "third-party/nsh/docs/dev/native-pipeline.md": "07f803365af6ac90d5247c70d2e85ce9e03393f0e9e122c46690f85e381a0c55",
-    "third-party/nsh/docs/dev/performance.md": "c40be7a2052abf26542b024775ec10219247af26a473bb3f784c02e4f4a5209f",
-    "third-party/nsh/docs/dev/pitfalls.md": "b8dc2e12cdb09531c2903898028fa963104f13fbbee34677db37762781f13343",
+    "third-party/nsh/docs/dev/performance.md": "9b763371132747a068259288cdd6019d7dfe37553d4413c0a9a330d9339f704a",
+    "third-party/nsh/docs/dev/pitfalls.md": "366400d2c07ab095536caa2907e0e5e451dcc68c2a08ccb88264cbc2fe23e24d",
     "third-party/nsh/docs/dev/reactive-components.md": "cd447fe8cfbe9026d7cadd5fa42d9c387e4c1b1b7e1c5adddd55c3f1beb94a4d",
     "third-party/nsh/docs/dev/rust-classes.md": "1f70015213dbcf56e6d6e4641019092003c22eb57ca324d9c4a85578b4468139",
     "third-party/nsh/docs/dev/rust-modules.md": "f793bfb12272ded93e80876fd2c72ff8dc0f1bd0337977f39b750af495a83592",
@@ -2297,7 +2298,7 @@
     "third-party/nsh/third-party/icy_sixel/src/encoder.rs": "9f2941a073f3df5d1216bb4f71487a3c43b5ceded1ec88cd13a804b4d7e31999",
     "third-party/nsh/third-party/icy_sixel/src/lib.rs": "126c4edada7c430377a73839c9da93cae95f9191be69c1401fb4ac1cad9cbf5e",
     "third-party/nsh/third-party/icy_sixel/src/sixel_image.rs": "1e968370dd0ff8c69751213a1ed11cfce6783feabb30c2b5074ac1c4ba3d8503",
-    "third-party/nsh/upstream.json": "c736356124396bf08bd04f250dcf381e839992aed682715e252fcde3d94412c2",
+    "third-party/nsh/upstream.json": "abd7c1558fd84b72a24db1d5b76b5d4009f484081a17af07ab703899daca816f",
     "third-party/nsh/website/DESIGN.md": "484ab5b78233c1c57b0d46e631d2341031b2b34a74740abbb88a275b4c8134b7",
     "third-party/nsh/website/README.md": "50de9faed8cd3b0751991d6168ac6e33a6437dfd36aa15dc30d35b13dfb94284",
     "third-party/nsh/website/astro.config.mjs": "0cfae54c7791bd915865dc535669b311e4f76001efed09fdb2772d332812924f",
diff --git a/third-party/nsh/AGENTS.md b/third-party/nsh/AGENTS.md
index f1c9af0d10..82913a4679 100644
--- a/third-party/nsh/AGENTS.md
+++ b/third-party/nsh/AGENTS.md
@@ -26,7 +26,7 @@ the module docs of the code (`//!`, Javadoc), which are the reference for how a
 
 ```
 crates/
-  nsh-core/            Platform-independent core: typed units and geometry (`geometry/`, as GPUI's), shadow tree, styles (RN semantics), Taffy layout (flex + grid), text merging, list virtualization,
+  nsh-core/            Platform-independent core: typed units and geometry (`geometry/`, as GPUI's), shadow tree, styles (RN semantics), Taffy layout (flex + grid + block, on the core's own layout tree: `core/layout_tree.rs`), text merging, list virtualization,
                        mount items; CSS values (`css.rs`: colors, lengths, shadows, gradients, filters), effects (`effects.rs`), Tailwind (`tailwind/`);
                        the reactive graph both sides run on (`reactive.rs`, Solid's algorithm, generic over the host), the loads of
                        resources both faces share (`resource.rs`); the selection across the texts of a selection area (`selection.rs`, the platforms run it);
@@ -332,8 +332,8 @@ when changing text measurement, clipping, pointer input or scripts (`docs/dev/tu
   behavior and defaults stay (new behavior is opt-in); formats (`.nshpkg`, certificates) are read forever. The record
   and its checks live in that repository, never here: a change to NSH cannot rewrite them, and the only removals are
   security closures in its `closed.txt`. No release is recorded yet, so nothing is frozen: what is on `main` may still
-  change. A new API lands with its test, a case in `tests/compat/cases/<domain>/` whose script checks it
-  (`cargo xtask compat`; `docs/dev/compatibility.md`).
+  change until 0.3.0, the first release recorded. A new API lands with its test, a case in
+  `tests/compat/cases/<domain>/` whose script checks it (`cargo xtask compat`; `docs/dev/compatibility.md`).
 - **No styles in NSH.** Nothing NSH ships has a look of its own: no default colors, paddings, radii, fonts or variants
   in `nsh-base`'s components (its theme's default tokens, shadcn/ui's, are the app's to draw with; the one
   exception is `TextView`'s default reading typography, its colors the theme's shadcn tokens (alerts GitHub's alert
diff --git a/third-party/nsh/Cargo.toml b/third-party/nsh/Cargo.toml
index 0335cf9e35..565015a9fd 100644
--- a/third-party/nsh/Cargo.toml
+++ b/third-party/nsh/Cargo.toml
@@ -158,7 +158,7 @@ pango = "0.22"
 pangocairo = "0.22"
 # The Linux backend's web views (its feature `webview`): WebKitGTK 6.0 (2.40+).
 webkit6 = "0.6"
-taffy = { version = "0.15", default-features = false, features = ["std", "taffy_tree", "flexbox", "grid", "block_layout", "content_size"] }
+taffy = { version = "0.15", default-features = false, features = ["std", "flexbox", "grid", "block_layout", "content_size"] }
 # Features per crate: the runtime's binaries get only what they use.
 tokio = { version = "1", default-features = false }
 
diff --git a/third-party/nsh/SOURCE.md b/third-party/nsh/SOURCE.md
index edd1ba3b47..a52c40b41e 100644
--- a/third-party/nsh/SOURCE.md
+++ b/third-party/nsh/SOURCE.md
@@ -26,7 +26,7 @@ at `de176695240d550f2b303643b8019146c7989e7f`. The upstream project is
 
 `upstream.json` keeps that initial `repository` and `revision`. Its
 `upstream_revision` records the latest integrated `longbridge/nsh` main revision:
-`de52bb3f9c236c48f0414f1bb36c17b92914ff91`. The first upstream synchronization
+`111b3a933319fadb0f1da9a59909ca526f256386`. The first upstream synchronization
 starts from `6bb0b7908043132067456e4a21dd57c90a0bdce5`, already included in the
 initial fork import.
 
@@ -38,6 +38,12 @@ the README retains the terminal backend description). This is a mixed-source
 vendor tree, not a claim that one upstream revision contains the fork features.
 The source, tests, resources, original licenses and independent workspaces are
 retained. `SOURCE.md` and `upstream.json` are Maka's provenance files.
+The local hint-observation integration retains an allocation lifetime separate
+from the upstream layout tree's reusable slab index. This prevents a delayed
+pointer observation from activating a newly allocated hint or anchor; the
+existing hint-lifetime regression covers release and reuse. Keep this adaptation
+until an upstream owner-identity mechanism provides the same guarantee.
+
 Maka's workspace uses these local crates, including NSH's offscreen renderer and
 image codec; changing them here changes the code built and tested with Maka.
 
diff --git a/third-party/nsh/crates/nsh-core/src/core/classes.rs b/third-party/nsh/crates/nsh-core/src/core/classes.rs
index aebcd73169..aa1ab3ff11 100644
--- a/third-party/nsh/crates/nsh-core/src/core/classes.rs
+++ b/third-party/nsh/crates/nsh-core/src/core/classes.rs
@@ -349,8 +349,7 @@ impl<M: TextEngine> Core<M> {
         let node = &self.nodes[key as usize];
         let mut style = node.mounted_view_style();
         if let Some(layout) = node.layout {
-            style.hidden |=
-                self.layout.style(layout).expect("taffy style").display == taffy::Display::None;
+            style.hidden |= self.layout.style(layout).display == taffy::Display::None;
         }
         let parent = node.parent;
         if parent == NO_KEY || node.inline {
diff --git a/third-party/nsh/crates/nsh-core/src/core/layout_tree.rs b/third-party/nsh/crates/nsh-core/src/core/layout_tree.rs
new file mode 100644
index 0000000000..7da42b2415
--- /dev/null
+++ b/third-party/nsh/crates/nsh-core/src/core/layout_tree.rs
@@ -0,0 +1,560 @@
+//! The core's layout tree: Taffy's algorithms (flexbox, grid, block, out of
+//! flow) run over nodes the core keeps, through Taffy's low-level traits,
+//! as `TaffyTree` runs them over its own (`TaffyView`, which this follows
+//! call for call). What the core does differently is its own:
+//!
+//! - **dirty marking goes up to the root, always.** `TaffyTree::mark_dirty`
+//!   stops at the first node whose cache is already empty, taking its
+//!   ancestors for dirty too; a node of a `display: none` subtree or an
+//!   absolutely positioned one always has an empty cache while the nodes
+//!   above keep theirs, so a change there was never laid out.
+//! - **no hoisted children.** Taffy records, on each containing block, the
+//!   absolutely positioned nodes it laid out, for its rounding pass alone;
+//!   the core rounds layouts itself (`core::rounded`), so nothing is kept,
+//!   and nothing can go stale.
+//! - **no rounding**: [`LayoutTree::unrounded_layout`] is what the
+//!   algorithms computed.
+//! - **relayout boundaries.** A change inside a node whose size its content
+//!   cannot change (fixed points both ways, in a flex container, not
+//!   shrinking nor aligned by its baseline: [`LayoutTree::is_boundary`])
+//!   dirties nothing above it: [`LayoutTree::relayout_boundaries`] lays its
+//!   subtree out again with what its parent gave it last time, and only
+//!   when what it gives back differs (its overflow, say) is its parent laid
+//!   out again. A large page with values changing often costs what changes,
+//!   not what the page is.
+
+use slab::Slab;
+use taffy::{
+    AvailableSpace, BlockContext, Cache, CacheTree, DetailedGridInfo, DetailedLayoutInfo, Display,
+    Layout, LayoutBlockContainer, LayoutContainingBlock, LayoutFlexboxContainer,
+    LayoutGridContainer, LayoutInput, LayoutOutput, LayoutPartialTree, NodeId, RunMode, Size,
+    Style, TraversePartialTree, TraverseTree, compute_block_layout, compute_cached_layout,
+    compute_flexbox_layout, compute_grid_layout, compute_hidden_layout, compute_oof_layout,
+    compute_root_layout,
+};
+
+/// A node: its style, place in the tree, cache and layout, and what
+/// measures it when it is a leaf.
+struct LayoutNode<C> {
+    style: Style,
+    /// Stable owner identity, distinct from the reusable slab index.
+    lifetime: u64,
+    parent: Option<NodeId>,
+    children: Vec<NodeId>,
+    cache: Cache,
+    unrounded: Layout,
+    detailed: DetailedLayoutInfo,
+    context: Option<C>,
+    /// The core's node it lays out ([`LayoutTree::set_tag`]).
+    tag: Option<u32>,
+    /// What its parent's last full layout gave it and what it gave back:
+    /// what a boundary is laid out again with, and compared to.
+    last: Option<(LayoutInput, LayoutOutput)>,
+    /// It is a boundary waiting in [`LayoutTree::dirty_boundaries`].
+    queued: bool,
+}
+
+impl<C> LayoutNode<C> {
+    fn new(style: Style, context: Option<C>, lifetime: u64) -> Self {
+        LayoutNode {
+            style,
+            lifetime,
+            parent: None,
+            children: Vec::new(),
+            cache: Cache::new(),
+            unrounded: Layout::new(),
+            detailed: DetailedLayoutInfo::None,
+            context,
+            tag: None,
+            last: None,
+            queued: false,
+        }
+    }
+}
+
+/// The layout nodes of a core, each with a context `C` its leaves are
+/// measured with.
+pub(crate) struct LayoutTree<C> {
+    nodes: Slab<LayoutNode<C>>,
+    next_lifetime: u64,
+    /// Boundaries a change inside made dirty, to lay out again.
+    dirty_boundaries: Vec<NodeId>,
+}
+
+fn key(node: NodeId) -> usize {
+    usize::from(node)
+}
+
+impl<C> LayoutTree<C> {
+    pub(crate) fn with_capacity(capacity: usize) -> Self {
+        LayoutTree {
+            nodes: Slab::with_capacity(capacity),
+            next_lifetime: 1,
+            dirty_boundaries: Vec::new(),
+        }
+    }
+
+    /// Identifies this allocation even when a removed node's slot is reused.
+    /// Zero remains reserved for a missing owner in native hint watches.
+    pub(crate) fn lifetime(&self, node: NodeId) -> u64 {
+        self.nodes[key(node)].lifetime
+    }
+
+    fn insert(&mut self, style: Style, context: Option<C>) -> NodeId {
+        let lifetime = self.next_lifetime;
+        self.next_lifetime = lifetime.checked_add(1).expect("layout lifetime exhausted");
+        NodeId::from(self.nodes.insert(LayoutNode::new(style, context, lifetime)))
+    }
+
+    /// A new detached node.
+    pub(crate) fn new_leaf(&mut self, style: Style) -> NodeId {
+        self.insert(style, None)
+    }
+
+    /// A new detached node measured with `context`.
+    pub(crate) fn new_leaf_with_context(&mut self, style: Style, context: C) -> NodeId {
+        self.insert(style, Some(context))
+    }
+
+    /// A new detached node holding `children` (detached nodes).
+    pub(crate) fn new_with_children(&mut self, style: Style, children: &[NodeId]) -> NodeId {
+        let id = self.insert(style, None);
+        for &child in children {
+            self.nodes[key(child)].parent = Some(id);
+        }
+        self.nodes[key(id)].children = children.to_vec();
+        id
+    }
+
+    /// Records that `node` lays out the core's node `tag`.
+    pub(crate) fn set_tag(&mut self, node: NodeId, tag: u32) {
+        self.nodes[key(node)].tag = Some(tag);
+    }
+
+    /// Frees `node`, detaching it from its parent; its children are left
+    /// without one.
+    pub(crate) fn remove(&mut self, node: NodeId) {
+        if let Some(parent) = self.nodes[key(node)].parent {
+            self.nodes[key(parent)].children.retain(|&c| c != node);
+            self.mark_dirty(parent);
+        }
+        if self.nodes[key(node)].queued {
+            self.dirty_boundaries.retain(|&b| b != node);
+        }
+        let removed = self.nodes.remove(key(node));
+        for child in removed.children {
+            self.nodes[key(child)].parent = None;
+        }
+    }
+
+    pub(crate) fn style(&self, node: NodeId) -> &Style {
+        &self.nodes[key(node)].style
+    }
+
+    /// Replaces `node`'s style: its own size may change, so its parent is
+    /// laid out again (whether it is a boundary or not).
+    pub(crate) fn set_style(&mut self, node: NodeId, style: Style) {
+        let n = &mut self.nodes[key(node)];
+        n.style = style;
+        n.cache.clear();
+        if let Some(parent) = n.parent {
+            self.mark_dirty(parent);
+        }
+    }
+
+    /// Replaces what measures `node`.
+    pub(crate) fn set_node_context(&mut self, node: NodeId, context: Option<C>) {
+        self.nodes[key(node)].context = context;
+        self.mark_dirty(node);
+    }
+
+    pub(crate) fn get_node_context(&self, node: NodeId) -> Option<&C> {
+        self.nodes[key(node)].context.as_ref()
+    }
+
+    pub(crate) fn get_node_context_mut(&mut self, node: NodeId) -> Option<&mut C> {
+        self.nodes[key(node)].context.as_mut()
+    }
+
+    pub(crate) fn parent(&self, node: NodeId) -> Option<NodeId> {
+        self.nodes[key(node)].parent
+    }
+
+    #[cfg(test)]
+    pub(crate) fn children(&self, node: NodeId) -> &[NodeId] {
+        &self.nodes[key(node)].children
+    }
+
+    /// Appends `child` (detached) to `parent`'s children.
+    pub(crate) fn add_child(&mut self, parent: NodeId, child: NodeId) {
+        let index = self.nodes[key(parent)].children.len();
+        self.insert_child_at_index(parent, index, child);
+    }
+
+    /// Inserts `child` (detached) into `parent` at `index`.
+    pub(crate) fn insert_child_at_index(&mut self, parent: NodeId, index: usize, child: NodeId) {
+        self.nodes[key(child)].parent = Some(parent);
+        self.nodes[key(parent)].children.insert(index, child);
+        self.mark_dirty(parent);
+    }
+
+    /// Detaches `child` from `parent`.
+    pub(crate) fn remove_child(&mut self, parent: NodeId, child: NodeId) {
+        self.nodes[key(parent)].children.retain(|&c| c != child);
+        self.nodes[key(child)].parent = None;
+        self.mark_dirty(parent);
+    }
+
+    /// Something in `node` changed (its children, what measures it):
+    /// clears the cached layouts of `node` and of every node above it up to
+    /// the root (never stopping at a node whose cache is already empty: see
+    /// the module's docs), or up to the first boundary on the way, `node`
+    /// included, which is queued to be laid out again on its own.
+    pub(crate) fn mark_dirty(&mut self, node: NodeId) {
+        let mut at = Some(node);
+        while let Some(node) = at {
+            self.nodes[key(node)].cache.clear();
+            if self.is_boundary(node) {
+                let n = &mut self.nodes[key(node)];
+                if !n.queued {
+                    n.queued = true;
+                    self.dirty_boundaries.push(node);
+                }
+                return;
+            }
+            at = self.nodes[key(node)].parent;
+        }
+    }
+
+    /// Whether what changes inside `node` cannot change what its parent's
+    /// layout takes from it, so that layout stands: it has a size of fixed
+    /// points both ways in a flex container, and everything the flex
+    /// algorithm takes from an item follows from that size alone. Its flex
+    /// basis is `auto`, its main size (a percentage basis against a parent
+    /// of no definite size, or another length under a content-based
+    /// automatic minimum, would follow its content); it does not shrink (its
+    /// automatic minimum, its content's, is never above its size, so it
+    /// only matters to shrinking); growing takes the free space its parent
+    /// and siblings leave; it is not aligned by its baseline; its intrinsic
+    /// contributions are its size. And its parent laid it out before. What
+    /// it gives back that the size does not fix (overflow, margins) is
+    /// compared after ([`LayoutTree::relayout_boundaries`]).
+    fn is_boundary(&self, node: NodeId) -> bool {
+        let n = &self.nodes[key(node)];
+        let Some(parent) = n.parent else {
+            return false;
+        };
+        let fixed = |d: taffy::Dimension| d.into_raw().tag() == taffy::CompactLength::LENGTH_TAG;
+        let (style, parent) = (&n.style, &self.nodes[key(parent)].style);
+        let baseline = |a: taffy::AlignItems| a.keyword() == taffy::AlignItemsKeyword::Baseline;
+        n.last.is_some()
+            && parent.display == Display::Flex
+            && style.display != Display::None
+            && fixed(style.size.width)
+            && fixed(style.size.height)
+            && style.flex_shrink == 0.0
+            && style.flex_basis == taffy::Dimension::auto()
+            && !baseline(parent.align_items)
+            && !style.align_self.is_some_and(baseline)
+    }
+
+    /// Whether `node` has no cached layout: it changed, or something in it
+    /// did, since it was last laid out.
+    pub(crate) fn dirty(&self, node: NodeId) -> bool {
+        self.nodes[key(node)].cache.is_empty()
+    }
+
+    /// `node`'s layout as the algorithms computed it, relative to its
+    /// parent, unrounded.
+    pub(crate) fn unrounded_layout(&self, node: NodeId) -> &Layout {
+        &self.nodes[key(node)].unrounded
+    }
+
+    /// Lays out the boundaries a change inside made dirty, each with what
+    /// its parent gave it last time; one that gives back what it gave
+    /// before (its size, overflow, baselines, margins) leaves its parent's
+    /// layout standing, another dirties its parent, as a change of its own
+    /// would. Returns the tags of those laid out on their own: their
+    /// subtrees' frames are new, nothing else's is.
+    pub(crate) fn relayout_boundaries<M>(&mut self, measure: M) -> Vec<u32>
+    where
+        M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+    {
+        let mut relaid = Vec::new();
+        if self.dirty_boundaries.is_empty() {
+            return relaid;
+        }
+        let mut view = View {
+            tree: self,
+            measure,
+        };
+        while let Some(node) = view.tree.dirty_boundaries.pop() {
+            let n = &mut view.tree.nodes[key(node)];
+            n.queued = false;
+            let parent = n.parent;
+            // Moved or restyled since: no longer one on its own.
+            if !view.tree.is_boundary(node) {
+                if let Some(parent) = parent {
+                    view.tree.mark_dirty(parent);
+                }
+                continue;
+            }
+            let (input, before) = view.tree.nodes[key(node)].last.clone().expect("laid out");
+            let after = view.compute_child_layout(node, input, None);
+            if after == before {
+                relaid.extend(view.tree.nodes[key(node)].tag);
+            } else if let Some(parent) = parent {
+                view.tree.mark_dirty(parent);
+            }
+        }
+        relaid
+    }
+
+    /// Lays out the tree under `root` within `available`, measuring leaves
+    /// with `measure` (`taffy::TaffyTree::compute_layout_with_measure`'s).
+    pub(crate) fn compute_layout_with_measure<M>(
+        &mut self,
+        root: NodeId,
+        available: Size<AvailableSpace>,
+        measure: M,
+    ) where
+        M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+    {
+        let mut view = View {
+            tree: self,
+            measure,
+        };
+        compute_root_layout(&mut view, root, available);
+    }
+}
+
+/// The tree being laid out, with what measures its leaves: what Taffy's
+/// algorithms run over.
+struct View<'t, C, M> {
+    tree: &'t mut LayoutTree<C>,
+    measure: M,
+}
+
+impl<C, M> View<'_, C, M>
+where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+{
+    fn node(&self, node: NodeId) -> &LayoutNode<C> {
+        &self.tree.nodes[key(node)]
+    }
+
+    /// What both `compute_child_layout` and `compute_block_child_layout`
+    /// run (`TaffyView::compute_child_layout`).
+    fn compute_child_layout(
+        &mut self,
+        node: NodeId,
+        inputs: LayoutInput,
+        block: Option<&mut BlockContext<'_>>,
+    ) -> LayoutOutput {
+        // An ancestor is `display: none`: hidden whatever its own display.
+        if inputs.run_mode == RunMode::PerformHiddenLayout {
+            self.tree.nodes[key(node)].last = None;
+            return compute_hidden_layout(self, node);
+        }
+        let output = compute_cached_layout(self, node, inputs, |view, node, inputs| {
+            let display = view.node(node).style.display;
+            let has_children = !view.node(node).children.is_empty();
+            let mut output = match (display, has_children) {
+                (Display::None, _) => compute_hidden_layout(view, node),
+                (Display::Block, true) => compute_block_layout(view, node, inputs, block),
+                (Display::FlowRoot, true) => compute_block_layout(view, node, inputs, None),
+                (Display::Flex, true) => compute_flexbox_layout(view, node, inputs),
+                (Display::Grid, true) => compute_grid_layout(view, node, inputs),
+                (_, false) => {
+                    let n = &mut view.tree.nodes[key(node)];
+                    (view.measure)(inputs, node, n.context.as_mut(), &n.style)
+                }
+            };
+            // The out-of-flow nodes this one is the containing block of, in
+            // full layout passes only, as Taffy's tree does.
+            if inputs.run_mode == RunMode::PerformLayout {
+                compute_oof_layout(view, node, &mut output);
+            }
+            output
+        });
+        if inputs.run_mode == RunMode::PerformLayout {
+            self.tree.nodes[key(node)].last = Some((inputs, output.clone()));
+        }
+        output
+    }
+}
+
+impl<C, M> TraversePartialTree for View<'_, C, M>
+where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+{
+    type ChildIter<'a>
+        = std::iter::Copied<std::slice::Iter<'a, NodeId>>
+    where
+        Self: 'a;
+
+    fn child_ids(&self, parent: NodeId) -> Self::ChildIter<'_> {
+        self.node(parent).children.iter().copied()
+    }
+
+    fn child_count(&self, parent: NodeId) -> usize {
+        self.node(parent).children.len()
+    }
+
+    fn get_child_id(&self, parent: NodeId, index: usize) -> NodeId {
+        self.node(parent).children[index]
+    }
+}
+
+impl<C, M> TraverseTree for View<'_, C, M> where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput
+{
+}
+
+impl<C, M> LayoutPartialTree for View<'_, C, M>
+where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+{
+    type CoreContainerStyle<'a>
+        = &'a Style
+    where
+        Self: 'a;
+
+    // `Style`'s default, Taffy's `DefaultCheapStr` with `std`.
+    type CustomIdent = String;
+
+    fn get_core_container_style(&self, node: NodeId) -> Self::CoreContainerStyle<'_> {
+        &self.node(node).style
+    }
+
+    fn set_unrounded_layout(&mut self, node: NodeId, layout: &Layout) {
+        self.tree.nodes[key(node)].unrounded = *layout;
+    }
+
+    fn resolve_calc_value(&self, _: *const (), _: f32) -> f32 {
+        0.0
+    }
+
+    fn compute_child_layout(&mut self, node: NodeId, inputs: LayoutInput) -> LayoutOutput {
+        View::compute_child_layout(self, node, inputs, None)
+    }
+}
+
+impl<C, M> LayoutContainingBlock for View<'_, C, M>
+where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+{
+    type OofItemStyle<'a>
+        = &'a Style
+    where
+        Self: 'a;
+
+    fn get_oof_item_style(&self, node: NodeId) -> Self::OofItemStyle<'_> {
+        &self.node(node).style
+    }
+
+    // Kept for Taffy's rounding pass alone, which the core does not run.
+    fn clear_hoisted_children(&mut self, _: NodeId) {}
+
+    fn add_hoisted_children(&mut self, _: NodeId, _: &[NodeId]) {}
+
+    fn get_detailed_layout_info(&self, node: NodeId) -> &DetailedLayoutInfo {
+        &self.node(node).detailed
+    }
+}
+
+impl<C, M> CacheTree for View<'_, C, M>
+where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+{
+    fn cache_get(&mut self, node: NodeId, input: &LayoutInput) -> Option<LayoutOutput> {
+        self.tree.nodes[key(node)].cache.get(input)
+    }
+
+    fn cache_store(&mut self, node: NodeId, input: &LayoutInput, output: LayoutOutput) {
+        self.tree.nodes[key(node)].cache.store(input, output);
+    }
+
+    fn cache_clear(&mut self, node: NodeId) {
+        self.tree.nodes[key(node)].cache.clear();
+    }
+}
+
+impl<C, M> LayoutBlockContainer for View<'_, C, M>
+where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+{
+    type BlockContainerStyle<'a>
+        = &'a Style
+    where
+        Self: 'a;
+    type BlockItemStyle<'a>
+        = &'a Style
+    where
+        Self: 'a;
+
+    fn get_block_container_style(&self, node: NodeId) -> Self::BlockContainerStyle<'_> {
+        &self.node(node).style
+    }
+
+    fn get_block_child_style(&self, child: NodeId) -> Self::BlockItemStyle<'_> {
+        &self.node(child).style
+    }
+
+    fn compute_block_child_layout(
+        &mut self,
+        node: NodeId,
+        inputs: LayoutInput,
+        block: Option<&mut BlockContext<'_>>,
+    ) -> LayoutOutput {
+        View::compute_child_layout(self, node, inputs, block)
+    }
+}
+
+impl<C, M> LayoutFlexboxContainer for View<'_, C, M>
+where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+{
+    type FlexboxContainerStyle<'a>
+        = &'a Style
+    where
+        Self: 'a;
+    type FlexboxItemStyle<'a>
+        = &'a Style
+    where
+        Self: 'a;
+
+    fn get_flexbox_container_style(&self, node: NodeId) -> Self::FlexboxContainerStyle<'_> {
+        &self.node(node).style
+    }
+
+    fn get_flexbox_child_style(&self, child: NodeId) -> Self::FlexboxItemStyle<'_> {
+        &self.node(child).style
+    }
+}
+
+impl<C, M> LayoutGridContainer for View<'_, C, M>
+where
+    M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+{
+    type GridContainerStyle<'a>
+        = &'a Style
+    where
+        Self: 'a;
+    type GridItemStyle<'a>
+        = &'a Style
+    where
+        Self: 'a;
+
+    fn get_grid_container_style(&self, node: NodeId) -> Self::GridContainerStyle<'_> {
+        &self.node(node).style
+    }
+
+    fn get_grid_child_style(&self, child: NodeId) -> Self::GridItemStyle<'_> {
+        &self.node(child).style
+    }
+
+    fn set_detailed_grid_info(&mut self, node: NodeId, info: DetailedGridInfo) {
+        self.tree.nodes[key(node)].detailed = DetailedLayoutInfo::Grid(Box::new(info));
+    }
+}
diff --git a/third-party/nsh/crates/nsh-core/src/core/mod.rs b/third-party/nsh/crates/nsh-core/src/core/mod.rs
index 8b02acb581..ab45f8c35e 100644
--- a/third-party/nsh/crates/nsh-core/src/core/mod.rs
+++ b/third-party/nsh/crates/nsh-core/src/core/mod.rs
@@ -1,5 +1,6 @@
 mod classes;
 mod components;
+mod layout_tree;
 mod list;
 mod measure;
 mod overlay;
@@ -16,10 +17,13 @@ mod views;
 use std::{rc::Rc, sync::Arc};
 
 use slab::Slab;
-use taffy::{AvailableSpace, NodeId as LayoutId, Overflow, Position, TaffyTree};
+use taffy::{AvailableSpace, NodeId as LayoutId, Overflow, Position};
 
-use self::list::{ExtraKind, Follow, ListState, PendingScroll, PinTarget, StickyRange};
 pub use self::measure::Measurement;
+use self::{
+    layout_tree::LayoutTree,
+    list::{ExtraKind, Follow, ListState, PendingScroll, PinTarget, StickyRange},
+};
 use crate::{
     animation::AnimationProp,
     geometry::{Bounds, Percentage, Point, Points, Size, rect, size_pt},
@@ -749,7 +753,7 @@ pub struct Core<M: TextEngine> {
     nodes: Slab<Node>,
     /// Node id -> key + 1 (0 = none). Ids are dense, so this is a flat vec.
     slots: Vec<u32>,
-    layout: TaffyTree<LeafContext<M::Prepared>>,
+    layout: LayoutTree<LeafContext<M::Prepared>>,
     text_engine: Rc<M>,
     measure_cache: MeasureCache,
     /// Text blocks whose final layout must be (re)made after layout.
@@ -883,17 +887,15 @@ impl<M: TextEngine + 'static> Core<M> {
 
 impl<M: TextEngine> Core<M> {
     fn with_engine(text_engine: M, viewport: Size<Points>) -> Self {
-        let mut layout = TaffyTree::with_capacity(256);
-        // The core rounds layouts itself (`rounded`): Taffy's pass walks the
-        // absolutely positioned nodes through lists it can leave stale.
-        layout.disable_rounding();
-        let root_layout = layout.new_leaf(root_style(viewport)).expect("taffy root");
+        let mut layout = LayoutTree::with_capacity(256);
+        let root_layout = layout.new_leaf(root_style(viewport));
         let mut nodes = Slab::with_capacity(256);
         let mut root = Node::new(ROOT_ID, Kind::View);
         root.layout = Some(root_layout);
         // The platform owns the root view.
         root.created = true;
         let root = nodes.insert(root) as Key;
+        layout.set_tag(root_layout, root);
         let mut slots = vec![0; 256];
         slots[ROOT_ID as usize] = root + 1;
         Self {
@@ -1109,9 +1111,7 @@ impl<M: TextEngine> Core<M> {
         if viewport != self.viewport {
             self.viewport = viewport;
             let root = self.nodes[self.root as usize].layout.expect("root layout");
-            self.layout
-                .set_style(root, root_style(viewport))
-                .expect("taffy root");
+            self.layout.set_style(root, root_style(viewport));
             self.needs_layout = true;
             self.restyle_where(crate::tailwind::deps::WIDTH | crate::tailwind::deps::HEIGHT);
             self.components_viewport(viewport);
@@ -1161,7 +1161,7 @@ impl<M: TextEngine> Core<M> {
             && *own != Some(size)
         {
             *own = Some(size);
-            self.layout.mark_dirty(layout).expect("taffy dirty");
+            self.layout.mark_dirty(layout);
             self.needs_layout = true;
         }
     }
@@ -1223,7 +1223,7 @@ impl<M: TextEngine> Core<M> {
             cx.min_content = None;
             cx.cache = None;
             cx.laid_out = None;
-            self.layout.mark_dirty(layout).expect("taffy dirty");
+            self.layout.mark_dirty(layout);
             self.needs_layout = true;
             if self.nodes[key as usize].kind == Kind::Text {
                 self.queue_text(key);
@@ -1500,6 +1500,9 @@ impl<M: TextEngine> Core<M> {
         let relayout = self.needs_layout;
         if relayout {
             self.needs_layout = false;
+            // Boundaries first: what they give back may dirty what is
+            // around them, laid out next.
+            let relaid = self.relayout_boundaries();
             // Only walk the main tree when it was laid out again (a change
             // inside a list cell leaves it untouched).
             if self.layout_main() {
@@ -1515,6 +1518,9 @@ impl<M: TextEngine> Core<M> {
             // Screens and modals, then list cells (lists may live in them).
             self.layout_roots();
             self.layout_cells();
+            for key in relaid {
+                self.emit_frames(key);
+            }
         }
         self.update_lists(relayout);
         self.reading_dirty = false;
@@ -1763,8 +1769,8 @@ impl<M: TextEngine> Core<M> {
         let layout = match leaf_context(kind) {
             Some(context) => self.layout.new_leaf_with_context(style.clone(), context),
             None => self.layout.new_leaf(style.clone()),
-        }
-        .expect("taffy leaf");
+        };
+        self.layout.set_tag(layout, key);
         self.nodes[key as usize].layout = Some(layout);
         self.became(key, style);
         Ok(())
@@ -1845,8 +1851,8 @@ impl<M: TextEngine> Core<M> {
         node.control = fresh.control;
         node.component = fresh.component;
         let layout = node.layout.expect("a view has a layout");
-        let _ = self.layout.set_node_context(layout, leaf_context(kind));
-        let style = self.layout.style(layout).cloned().unwrap_or_default();
+        self.layout.set_node_context(layout, leaf_context(kind));
+        let style = self.layout.style(layout).clone();
         self.became(key, style);
         if let Some((parent, before)) = at {
             let _ = self.insert(parent, id, before);
@@ -1885,7 +1891,7 @@ impl<M: TextEngine> Core<M> {
         if !same {
             *self.text_cx(layout) = TextContext::new(Arc::new(text));
         }
-        self.layout.mark_dirty(layout).expect("taffy dirty");
+        self.layout.mark_dirty(layout);
         self.needs_layout = true;
     }
 
@@ -1973,7 +1979,7 @@ impl<M: TextEngine> Core<M> {
         if node.fixed_view {
             style.position = Position::Fixed;
         }
-        self.layout.set_style(layout, style).expect("taffy style");
+        self.layout.set_style(layout, style);
         self.needs_layout = true;
     }
 
@@ -2087,7 +2093,7 @@ impl<M: TextEngine> Core<M> {
         let flipped = node.rtl != rtl;
         node.rtl = rtl;
         let display_changed = node.layout.is_some_and(|id| {
-            (self.layout.style(id).expect("taffy style").display == taffy::Display::None)
+            (self.layout.style(id).display == taffy::Display::None)
                 != (layout.display == taffy::Display::None)
         });
         // `objectPosition` is the image's (`MountItem::Image`), not the view's.
@@ -2147,7 +2153,7 @@ impl<M: TextEngine> Core<M> {
             node.base_layout = Some(Box::new(layout));
             self.sync_layout_style(key);
         } else if let Some(l) = node.layout {
-            self.layout.set_style(l, layout).expect("taffy style");
+            self.layout.set_style(l, layout);
             self.needs_layout = true;
         }
         let mut flags = 0;
@@ -2246,11 +2252,11 @@ impl<M: TextEngine> Core<M> {
             node.view_style = view;
             self.sync_layout_style(key);
         } else if let Some(l) = node.layout {
-            let mut layout = self.layout.style(l).expect("taffy style").clone();
+            let mut layout = self.layout.style(l).clone();
             crate::style::swap_sides(&mut layout, &mut view);
             layout.direction = direction;
             self.nodes[key as usize].view_style = view;
-            self.layout.set_style(l, layout).expect("taffy style");
+            self.layout.set_style(l, layout);
             self.needs_layout = true;
         }
         self.mark(key, dirty::VIEW);
@@ -2440,7 +2446,7 @@ impl<M: TextEngine> Core<M> {
                         && *natural != size
                     {
                         *natural = size;
-                        self.layout.mark_dirty(layout).expect("taffy dirty");
+                        self.layout.mark_dirty(layout);
                         self.needs_layout = true;
                     }
                 }
@@ -3326,8 +3332,7 @@ impl<M: TextEngine> Core<M> {
                     .layout
                     .expect("block parent has layout");
                 self.layout
-                    .insert_child_at_index(parent_layout, index as usize, child_layout)
-                    .expect("taffy insert");
+                    .insert_child_at_index(parent_layout, index as usize, child_layout);
             }
             self.needs_layout = true;
             // An overlay's native view goes to its root view
@@ -3406,9 +3411,7 @@ impl<M: TextEngine> Core<M> {
                 let parent_layout = self.nodes[parent as usize]
                     .layout
                     .expect("block parent has layout");
-                self.layout
-                    .remove_child(parent_layout, layout)
-                    .expect("taffy remove");
+                self.layout.remove_child(parent_layout, layout);
             }
             self.needs_layout = true;
             if emit_remove
@@ -3525,7 +3528,7 @@ impl<M: TextEngine> Core<M> {
             self.sticky_scrollers.retain(|&k| k != key);
         }
         if let Some((spacer, _)) = node.list.as_ref().and_then(|l| l.spacer) {
-            self.layout.remove(spacer).expect("taffy remove");
+            self.layout.remove(spacer);
         }
         if node.kind == Kind::Text {
             self.attachment_blocks.retain(|&k| k != key);
@@ -3552,7 +3555,7 @@ impl<M: TextEngine> Core<M> {
             }
         }
         if let Some(layout) = node.layout {
-            self.layout.remove(layout).expect("taffy remove");
+            self.layout.remove(layout);
         }
         if node.created || (node.inline && node.kind == Kind::Text) {
             // A virtual press target still has a native lifetime. Releasing
@@ -3661,7 +3664,7 @@ impl<M: TextEngine> Core<M> {
         if inline {
             // Block -> span: spans have no layout node or native view.
             if let Some(l) = node.layout.take() {
-                self.layout.remove(l).expect("taffy remove");
+                self.layout.remove(l);
             }
             if node.created {
                 node.created = false;
@@ -3670,11 +3673,9 @@ impl<M: TextEngine> Core<M> {
         } else {
             let style = node.base_layout.as_deref().cloned().unwrap_or_default();
             let cx = LeafContext::Text(TextContext::new(Arc::default()));
-            node.layout = Some(
-                self.layout
-                    .new_leaf_with_context(style, cx)
-                    .expect("taffy leaf"),
-            );
+            let layout = self.layout.new_leaf_with_context(style, cx);
+            self.layout.set_tag(layout, key);
+            node.layout = Some(layout);
             self.mark(key, dirty::TEXT_BUILD);
         }
     }
@@ -3707,7 +3708,6 @@ impl<M: TextEngine> Core<M> {
             || self
                 .layout
                 .style(node.layout.expect("native layout"))
-                .expect("taffy style")
                 .display
                 == taffy::Display::None
             || node.backdrop_material != crate::BackdropMaterial::default()
@@ -4315,7 +4315,7 @@ impl<M: TextEngine> Core<M> {
         if !same_layout {
             cx.layout_hash = cx.alignments.layout_hash(&cx.text);
             cx.cache = None;
-            self.layout.mark_dirty(layout).expect("taffy dirty");
+            self.layout.mark_dirty(layout);
             self.needs_layout = true;
         }
         true
@@ -4446,7 +4446,7 @@ impl<M: TextEngine> Core<M> {
                 inserted = true;
             }
             let layout = self.nodes[a as usize].layout.expect("attachment layout");
-            let dirty = self.layout.dirty(layout).unwrap_or(true);
+            let dirty = self.layout.dirty(layout);
             if dirty {
                 self.layout_root(
                     layout,
@@ -4498,7 +4498,7 @@ impl<M: TextEngine> Core<M> {
                 self.key(id).is_ok_and(|k| {
                     self.nodes[k as usize]
                         .layout
-                        .is_some_and(|l| self.layout.dirty(l).unwrap_or(true))
+                        .is_some_and(|l| self.layout.dirty(l))
                 })
             });
             if dirty {
@@ -4514,9 +4514,9 @@ impl<M: TextEngine> Core<M> {
     /// one way or the other on it).
     fn positioning_parent(&self, layout: LayoutId) -> Option<LayoutId> {
         let mut parent = self.layout.parent(layout)?;
-        match self.layout.style(layout).ok()?.position {
+        match self.layout.style(layout).position {
             Position::Absolute => {
-                while !self.layout.style(parent).ok()?.position.is_positioned() {
+                while !self.layout.style(parent).position.is_positioned() {
                     let Some(above) = self.layout.parent(parent) else {
                         break;
                     };
@@ -4569,7 +4569,7 @@ impl<M: TextEngine> Core<M> {
             height: AvailableSpace::Definite(self.viewport.height.0),
         };
         // A clean root has nothing new to lay out, nor frames to emit.
-        let main = self.layout.dirty(root).unwrap_or(true);
+        let main = self.layout.dirty(root);
         if main {
             self.layout_root(root, available);
         }
@@ -4794,13 +4794,13 @@ impl<M: TextEngine> Core<M> {
             ),
         };
         let changed = root.laid_out != Some((size, header));
-        if !changed && !self.layout.dirty(layout).unwrap_or(true) {
+        if !changed && !self.layout.dirty(layout) {
             return;
         }
         if kind == Kind::InputAccessory || (kind == Kind::HeaderItem && size.width > Points::ZERO) {
-            let mut style = self.layout.style(layout).expect("taffy style").clone();
+            let mut style = self.layout.style(layout).clone();
             style.size.width = taffy::Dimension::length(size.width.0);
-            self.layout.set_style(layout, style).expect("taffy style");
+            self.layout.set_style(layout, style);
         } else if kind != Kind::HeaderItem {
             let config_padding = match kind {
                 // Large titles and transparent headers lay the content out
@@ -4830,7 +4830,7 @@ impl<M: TextEngine> Core<M> {
                 };
                 style.padding.top = taffy::LengthPercentage::length(base + config_padding);
             }
-            self.layout.set_style(layout, style).expect("taffy style");
+            self.layout.set_style(layout, style);
         }
         let header_changed = root.laid_out.is_none_or(|(_, h)| h != header);
         self.nodes[key as usize]
@@ -4934,7 +4934,7 @@ impl<M: TextEngine> Core<M> {
                     Some(CellSlot::Item(_)) | None => item_cross,
                     Some(_) => extra_cross,
                 };
-                if resized || self.layout.dirty(cell_layout).unwrap_or(true) {
+                if resized || self.layout.dirty(cell_layout) {
                     self.layout_root(cell_layout, available(c));
                     self.nodes[cell as usize].relaid = true;
                 }
@@ -4949,43 +4949,19 @@ impl<M: TextEngine> Core<M> {
         let cache = &mut self.measure_cache;
         self.layout
             .compute_layout_with_measure(root, available, |input, _, cx, style| {
-                // Taffy resolves style sizes, padding, border, min/max and
-                // aspect ratio; only content is measured here.
-                taffy::compute_leaf_layout(
-                    input,
-                    style,
-                    |_, _| 0.0,
-                    |known, available| match cx {
-                        Some(LeafContext::Text(cx)) => {
-                            let max_width = if cx.text.no_wrap {
-                                None
-                            } else {
-                                known.width.or(match available.width {
-                                    AvailableSpace::Definite(w) => Some(w),
-                                    AvailableSpace::MinContent => {
-                                        Some(min_content_width(engine, cache, cx).0)
-                                    }
-                                    AvailableSpace::MaxContent => None,
-                                })
-                            };
-                            let measured = measure_text(engine, cache, cx, max_width);
-                            taffy::Size {
-                                width: known.width.unwrap_or(measured.width.0),
-                                height: known.height.unwrap_or(measured.height.0),
-                            }
-                        }
-                        Some(LeafContext::Image(Some(natural))) => {
-                            image_size(*natural, known.width, known.height)
-                        }
-                        Some(LeafContext::Native(Some(own))) => taffy::Size {
-                            width: known.width.unwrap_or(own.width.0),
-                            height: known.height.unwrap_or(own.height.0),
-                        },
-                        _ => taffy::Size::ZERO,
-                    },
-                )
-            })
-            .expect("taffy layout");
+                measure_leaf(engine, cache, input, cx, style)
+            });
+    }
+
+    /// Lays out the relayout boundaries a change inside made dirty
+    /// (`LayoutTree::relayout_boundaries`), returning those laid out on
+    /// their own: their subtrees' frames are new.
+    fn relayout_boundaries(&mut self) -> Vec<Key> {
+        let engine = &self.text_engine;
+        let cache = &mut self.measure_cache;
+        self.layout.relayout_boundaries(|input, _, cx, style| {
+            measure_leaf(engine, cache, input, cx, style)
+        })
     }
 
     /// Whether `key` is out of the layout: `display: none` on it or on a
@@ -4995,10 +4971,7 @@ impl<M: TextEngine> Core<M> {
         while k != NO_KEY {
             let n = &self.nodes[k as usize];
             if let Some(layout) = n.layout
-                && self
-                    .layout
-                    .style(layout)
-                    .is_ok_and(|s| s.display == taffy::Display::None)
+                && self.layout.style(layout).display == taffy::Display::None
             {
                 return true;
             }
@@ -5231,11 +5204,7 @@ impl<M: TextEngine> Core<M> {
         if !node.created {
             return;
         }
-        let hidden = hidden
-            || self
-                .layout
-                .style(layout)
-                .is_ok_and(|s| s.display == taffy::Display::None);
+        let hidden = hidden || self.layout.style(layout).display == taffy::Display::None;
         if hidden {
             // Cells keep independent layout caches. Re-emit their descendants
             // when shown again, even if their measured size has not changed.
@@ -5256,11 +5225,7 @@ impl<M: TextEngine> Core<M> {
         }
         let node = &self.nodes[key as usize];
         let unrounded = self.layout.unrounded_layout(layout);
-        let containing = if self
-            .layout
-            .style(layout)
-            .is_ok_and(|s| s.position.is_out_of_flow())
-        {
+        let containing = if self.layout.style(layout).position.is_out_of_flow() {
             self.layout_origin(layout)
         } else {
             origin
@@ -6259,6 +6224,50 @@ fn min_content_width<M: TextEngine>(
     width
 }
 
+/// A leaf's layout (`taffy::compute_leaf_layout`): Taffy resolves its
+/// style's sizes, padding, border, minimums, maximums and aspect ratio;
+/// only its content (a text, an image's natural size, a native
+/// component's own size) is measured here.
+fn measure_leaf<M: TextEngine>(
+    engine: &M,
+    cache: &mut MeasureCache,
+    input: taffy::LayoutInput,
+    cx: Option<&mut LeafContext<M::Prepared>>,
+    style: &taffy::Style,
+) -> taffy::LayoutOutput {
+    taffy::compute_leaf_layout(
+        input,
+        style,
+        |_, _| 0.0,
+        |known, available| match cx {
+            Some(LeafContext::Text(cx)) => {
+                let max_width = if cx.text.no_wrap {
+                    None
+                } else {
+                    known.width.or(match available.width {
+                        AvailableSpace::Definite(w) => Some(w),
+                        AvailableSpace::MinContent => Some(min_content_width(engine, cache, cx).0),
+                        AvailableSpace::MaxContent => None,
+                    })
+                };
+                let measured = measure_text(engine, cache, cx, max_width);
+                taffy::Size {
+                    width: known.width.unwrap_or(measured.width.0),
+                    height: known.height.unwrap_or(measured.height.0),
+                }
+            }
+            Some(LeafContext::Image(Some(natural))) => {
+                image_size(*natural, known.width, known.height)
+            }
+            Some(LeafContext::Native(Some(own))) => taffy::Size {
+                width: known.width.unwrap_or(own.width.0),
+                height: known.height.unwrap_or(own.height.0),
+            },
+            _ => taffy::Size::ZERO,
+        },
+    )
+}
+
 fn measure_text<M: TextEngine>(
     engine: &M,
     cache: &mut MeasureCache,
diff --git a/third-party/nsh/crates/nsh-core/src/core/overlay.rs b/third-party/nsh/crates/nsh-core/src/core/overlay.rs
index ea4ea99c76..d9774cfe91 100644
--- a/third-party/nsh/crates/nsh-core/src/core/overlay.rs
+++ b/third-party/nsh/crates/nsh-core/src/core/overlay.rs
@@ -156,8 +156,7 @@ impl<M: TextEngine> Core<M> {
     pub(super) fn overlay_made(&mut self, key: Key, layout: LayoutId) {
         let wrapper = self
             .layout
-            .new_with_children(taffy::Style::default(), &[layout])
-            .expect("taffy wrapper");
+            .new_with_children(taffy::Style::default(), &[layout]);
         if let Some(root) = self.nodes[key as usize].root.as_mut() {
             root.overlay = Some(Box::new(OverlayState {
                 config: OverlayConfig::default(),
@@ -229,7 +228,7 @@ impl<M: TextEngine> Core<M> {
         node.fixed_view = true;
         node.root = Some(Box::default());
         let layout = node.layout.expect("a view has a layout");
-        let style = self.layout.style(layout).cloned().unwrap_or_default();
+        let style = self.layout.style(layout).clone();
         self.nodes[key as usize].base_layout = Some(Box::new(style));
         self.roots.push(key);
         self.overlay_made(key, layout);
@@ -256,7 +255,7 @@ impl<M: TextEngine> Core<M> {
             .layout
             .expect("a view has a layout");
         if let Some((_, wrapper)) = state {
-            let _ = self.layout.remove_child(wrapper, layout);
+            self.layout.remove_child(wrapper, layout);
             self.overlay_released(key, wrapper);
         }
         self.roots.retain(|&k| k != key);
@@ -264,9 +263,7 @@ impl<M: TextEngine> Core<M> {
         node.fixed_view = false;
         node.root = None;
         if let Some(base) = node.base_layout.as_deref() {
-            self.layout
-                .set_style(layout, base.clone())
-                .expect("taffy style");
+            self.layout.set_style(layout, base.clone());
         }
         if !node.kind.derives_layout() {
             node.base_layout = None;
@@ -278,7 +275,7 @@ impl<M: TextEngine> Core<M> {
     /// layout node is gone already).
     pub(super) fn overlay_released(&mut self, key: Key, wrapper: LayoutId) {
         self.overlays.retain(|&k| k != key);
-        let _ = self.layout.remove(wrapper);
+        self.layout.remove(wrapper);
     }
 
     /// Node `id` is released: the nodes made for its `tooltip` go with it.
@@ -356,10 +353,7 @@ impl<M: TextEngine> Core<M> {
                 if !n.created {
                     return None;
                 }
-                let hidden = self
-                    .layout
-                    .style(layout)
-                    .is_ok_and(|s| s.display == taffy::Display::None);
+                let hidden = self.layout.style(layout).display == taffy::Display::None;
                 if hidden && !n.is_overlay() {
                     return None;
                 }
@@ -579,7 +573,10 @@ impl<M: TextEngine> Core<M> {
                     let callback = events.contains(EventKind::Show)
                         || events.contains(EventKind::Dismiss)
                         || self.hint_scope_has_callbacks(k);
-                    let layout = self.nodes[k as usize].layout.map(u64::from).unwrap_or(0);
+                    let layout = self.nodes[k as usize]
+                        .layout
+                        .map(|layout| self.layout.lifetime(layout))
+                        .unwrap_or(0);
                     hinting.push(Watch {
                         key: k,
                         id: self.nodes[k as usize].id,
@@ -597,10 +594,13 @@ impl<M: TextEngine> Core<M> {
                         layout,
                         hints: vec![(
                             self.nodes[key as usize].id,
-                            self.nodes[key as usize].layout.map(u64::from).unwrap_or(0),
+                            self.nodes[key as usize]
+                                .layout
+                                .map(|layout| self.layout.lifetime(layout))
+                                .unwrap_or(0),
                             self.overlay_host(key)
                                 .and_then(|host| self.nodes[host as usize].layout)
-                                .map(u64::from)
+                                .map(|layout| self.layout.lifetime(layout))
                                 .unwrap_or(0),
                         )],
                         token: None,
@@ -772,7 +772,7 @@ impl<M: TextEngine> Core<M> {
             .find(|watch| watch.token == Some(token))?;
         let anchor = self.nodes.get(watch.key as usize)?;
         if anchor.id != watch.id
-            || anchor.layout.map(u64::from) != Some(watch.layout)
+            || anchor.layout.map(|layout| self.layout.lifetime(layout)) != Some(watch.layout)
             || !self.passive_hint_anchor(watch.key)
         {
             return None;
@@ -791,10 +791,12 @@ impl<M: TextEngine> Core<M> {
                 let hint = &self.nodes[key as usize];
                 (
                     hint.id,
-                    hint.layout.map(u64::from).unwrap_or(0),
+                    hint.layout
+                        .map(|layout| self.layout.lifetime(layout))
+                        .unwrap_or(0),
                     self.overlay_host(key)
                         .and_then(|host| self.nodes[host as usize].layout)
-                        .map(u64::from)
+                        .map(|layout| self.layout.lifetime(layout))
                         .unwrap_or(0),
                 )
             })
@@ -1247,7 +1249,7 @@ impl<M: TextEngine> Core<M> {
         };
         let root = self.nodes[key as usize].root.as_ref().expect("root state");
         let changed = root.laid_out != Some((size, 0.0));
-        if !changed && !self.layout.dirty(wrapper).unwrap_or(true) {
+        if !changed && !self.layout.dirty(wrapper) {
             return;
         }
         if changed {
@@ -1258,7 +1260,7 @@ impl<M: TextEngine> Core<M> {
                 },
                 ..taffy::Style::default()
             };
-            self.layout.set_style(wrapper, style).expect("taffy style");
+            self.layout.set_style(wrapper, style);
         }
         self.nodes[key as usize]
             .root
diff --git a/third-party/nsh/crates/nsh-core/src/core/parent_viewport.rs b/third-party/nsh/crates/nsh-core/src/core/parent_viewport.rs
index ce0bb89314..8e5c173ddf 100644
--- a/third-party/nsh/crates/nsh-core/src/core/parent_viewport.rs
+++ b/third-party/nsh/crates/nsh-core/src/core/parent_viewport.rs
@@ -146,22 +146,19 @@ impl<M: TextEngine> Core<M> {
         list.set_parent(parent);
         if parent {
             let len = list.content_len();
-            let spacer = self
-                .layout
-                .new_leaf(spacer_style(list.horizontal, len))
-                .expect("taffy leaf");
-            self.layout.add_child(layout, spacer).expect("taffy child");
+            let spacer = self.layout.new_leaf(spacer_style(list.horizontal, len));
+            self.layout.add_child(layout, spacer);
             list.spacer = Some((spacer, len));
             self.parent_lists.push(key);
             self.outer_moved = true;
         } else {
             if let Some((spacer, _)) = list.spacer.take() {
-                self.layout.remove(spacer).expect("taffy remove");
+                self.layout.remove(spacer);
             }
             self.parent_lists.retain(|&k| k != key);
             self.forget_outer(key);
         }
-        self.layout.mark_dirty(layout).expect("taffy dirty");
+        self.layout.mark_dirty(layout);
         self.needs_layout = true;
         if let Some(Some(at)) = create {
             let kind = if parent {
@@ -202,8 +199,7 @@ impl<M: TextEngine> Core<M> {
         }
         list.spacer = Some((spacer, new));
         self.layout
-            .set_style(spacer, spacer_style(list.horizontal, new))
-            .expect("taffy style");
+            .set_style(spacer, spacer_style(list.horizontal, new));
         self.needs_layout = true;
         self.extent_changed = true;
     }
diff --git a/third-party/nsh/crates/nsh-core/src/core/tests.rs b/third-party/nsh/crates/nsh-core/src/core/tests.rs
index f5cfefe8dd..8f25e2b1a6 100644
--- a/third-party/nsh/crates/nsh-core/src/core/tests.rs
+++ b/third-party/nsh/crates/nsh-core/src/core/tests.rs
@@ -12285,6 +12285,101 @@ mod views {
         );
     }
 
+    /// How many rows [`Board`] shows, and how many of its values change
+    /// on each tick (the first cell of as many rows).
+    const BOARD_ROWS: usize = 80;
+    const BOARD_TICKING: usize = 40;
+
+    #[derive(Clone, Copy, PartialEq, Props)]
+    struct BoardProps {
+        /// Its cells have a size of their own: what their text says moves
+        /// nothing around them.
+        fixed: bool,
+    }
+
+    /// A page of high-frequency updates, a quote board: 80 rows of a
+    /// mark and five cells (about a thousand nodes), the first cells of 40
+    /// rows changing together on each press, their texts' lengths too.
+    #[component]
+    fn Board(props: BoardProps) -> impl IntoElement {
+        let fixed = props.fixed;
+        let ticks: Vec<Signal<u32>> = (0..BOARD_TICKING).map(|_| signal(0)).collect();
+        let pressed = ticks.clone();
+        let cell = move |content: Text| {
+            let cell = view().p_1().child(content);
+            if fixed {
+                cell.w(px(56.0)).h(px(24.0))
+            } else {
+                cell
+            }
+        };
+        view()
+            .on_press(move |_: &()| {
+                batch(|| {
+                    for tick in &pressed {
+                        tick.update(|t| *t += 7);
+                    }
+                })
+            })
+            .children((0..BOARD_ROWS).map(|row| {
+                let first = match ticks.get(row) {
+                    // A price: its length changes as it grows.
+                    Some(&tick) => {
+                        text(move || format!("{}.{:02}", 9 + tick.get() / 3, tick.get() % 100))
+                    }
+                    None => text(format!("{row}.00")),
+                };
+                view()
+                    .flex_row()
+                    .items_center()
+                    .p_1()
+                    .gap_2()
+                    .child(view().size(px(12.0)).bg(rgb(0x22aa66)))
+                    .child(cell(first))
+                    .children((1..5).map(move |col| cell(text(format!("r{row} c{col}")))))
+            }))
+    }
+
+    /// `cargo test --release -p nsh-core bench_high_frequency -- --ignored --nocapture`
+    ///
+    /// The cost of a commit when many values of a large page change at
+    /// once and often: [`Board`] with fixed cells and with cells that
+    /// take their text's size.
+    #[test]
+    #[ignore]
+    fn bench_high_frequency() {
+        for fixed in [true, false] {
+            let mut c = core();
+            c.mount_root(&Root::new::<Board>(move || BoardProps { fixed }));
+            let first = c.commit().unwrap();
+            let nodes = first
+                .items
+                .iter()
+                .filter(|i| matches!(i, MountItem::Create { .. }))
+                .count();
+            let host = root_host(&first);
+            let n = 2000;
+            let (mut event, mut commit) = (std::time::Duration::ZERO, std::time::Duration::ZERO);
+            let mut items = 0;
+            for _ in 0..n {
+                let start = std::time::Instant::now();
+                c.component_event(host, EventKind::Press, &EventPayload::None);
+                let mid = std::time::Instant::now();
+                items += c.commit().map_or(0, |b| b.items.len());
+                event += mid - start;
+                commit += mid.elapsed();
+            }
+            eprintln!(
+                "high frequency ({} cells, {nodes} views, {BOARD_TICKING} values a tick): \
+                 {:?} per event, {:?} per commit, {} mount items",
+                if fixed { "fixed" } else { "content-sized" },
+                event / n,
+                commit / n,
+                items / n as usize
+            );
+        }
+    }
+
     #[derive(Clone, Copy, PartialEq, Props)]
     struct ShownProps {
         value: crate::ui::Prop<SharedString>,
@@ -16299,7 +16394,7 @@ fn own_lists_have_no_spacer_and_scroll_themselves() {
     assert!(c.parent_lists.is_empty());
     let key = c.key(4).unwrap();
     let layout = c.nodes[key as usize].layout.unwrap();
-    assert!(c.layout.children(layout).unwrap().is_empty());
+    assert!(c.layout.children(layout).is_empty());
     let key = c.key(2).unwrap();
     assert!(
         c.nodes[key as usize]
@@ -17419,12 +17514,6 @@ mod layout_consistency {
         for step in 0..steps {
             // A commit takes a few changes at once, as a turn does.
             let changes = 1 + rng.below(3);
-            let changes = if std::env::var_os("ONE_CHANGE").is_some() {
-                let _ = changes;
-                1
-            } else {
-                changes
-            };
             for _ in 0..changes {
                 let (ops, what) = change(&mut rng, &mut model, fractions);
                 apply(&mut c, ops);
@@ -17956,3 +18045,73 @@ fn sticky_views_in_a_horizontal_scroll_view_stick_at_their_left() {
     let batch = c.commit().unwrap();
     assert_eq!(sticky_of(&batch, 2), Some(vec![(4, 50.0, 1050.0)]));
 }
+
+#[test]
+fn a_relayout_boundary_that_overflows_more_lays_its_parent_out_again() {
+    let mut c = core();
+    apply(
+        &mut c,
+        vec![
+            Op::Create {
+                id: 2,
+                tag: Tag::Scroll,
+            },
+            style_op(2, &[("height", StyleValue::Number(200.0))]),
+            Op::Create {
+                id: 3,
+                tag: Tag::View,
+            },
+            // A size of fixed points that does not shrink: a change inside
+            // cannot change it.
+            style_op(
+                3,
+                &[
+                    ("width", StyleValue::Number(100.0)),
+                    ("height", StyleValue::Number(50.0)),
+                    ("flexShrink", StyleValue::Number(0.0)),
+                ],
+            ),
+            Op::Create {
+                id: 4,
+                tag: Tag::View,
+            },
+            style_op(4, &[("height", StyleValue::Number(10.0))]),
+            Op::Insert {
+                parent: 3,
+                child: 4,
+                before: None,
+            },
+            Op::Insert {
+                parent: 2,
+                child: 3,
+                before: None,
+            },
+            Op::Insert {
+                parent: ROOT_ID,
+                child: 2,
+                before: None,
+            },
+        ],
+    );
+    c.commit();
+    // Within the box: its parent's layout stands.
+    apply(
+        &mut c,
+        vec![style_op(4, &[("height", StyleValue::Number(40.0))])],
+    );
+    let batch = c.commit().unwrap();
+    assert_eq!(frame_of(&batch, 4).unwrap().size.height, px(40.0));
+    assert!(frame_of(&batch, 3).is_none());
+    // Out of it: the scroll view's content takes the overflow.
+    apply(
+        &mut c,
+        vec![style_op(4, &[("height", StyleValue::Number(500.0))])],
+    );
+    let batch = c.commit().unwrap();
+    assert_eq!(frame_of(&batch, 4).unwrap().size.height, px(500.0));
+    let content = batch.items.iter().find_map(|i| match i {
+        MountItem::ScrollContent { id: 2, size } => Some(size.height.0),
+        _ => None,
+    });
+    assert_eq!(content, Some(500.0));
+}
diff --git a/third-party/nsh/docs/dev/compatibility.md b/third-party/nsh/docs/dev/compatibility.md
index abcd59bcf7..c9a8881c22 100644
--- a/third-party/nsh/docs/dev/compatibility.md
+++ b/third-party/nsh/docs/dev/compatibility.md
@@ -7,6 +7,7 @@ starting after an NSH update is a bug in NSH, never in the app.
 
 This page is the contract and how it is enforced. It holds from the first release recorded in
 `nsh-compat` ("Releasing") on, and for good, whatever the version number says; earlier versions made no promises.
+The first release recorded is 0.3.0: earlier ones are not recorded.
 
 ## What apps depend on
 
@@ -133,7 +134,7 @@ existing record, an app whose script fails, a `covers` claim that does not hold
 checks (`[<module>] <name>`, `[globals] <name>`, `[props] <name>`, `[events] <name>`, `[styles] <name>`; a
 component's own props and events as `[props] <Component>.<name>`, `[events] <Component>.<name>`; platform props as
 `ios.<name>`, ...). Commit the record in `nsh-compat` and open a pull request there; once it is merged, the release
-is frozen. The first release recorded starts the contract.
+is frozen. The first release recorded, 0.3.0, starts the contract.
 
 Before the first record, two things land in NSH:
 
diff --git a/third-party/nsh/docs/dev/performance.md b/third-party/nsh/docs/dev/performance.md
index 4dfb18c4ba..37febe6bf8 100644
--- a/third-party/nsh/docs/dev/performance.md
+++ b/third-party/nsh/docs/dev/performance.md
@@ -122,6 +122,15 @@ coalescing interval, with the latest coordinates kept independently per view and
   300 static rows and a counter text, one press per frame): the press and the binding it runs take ~0.3µs, the commit
   ~82µs (the text's relayout and the tree's layout pass), one mount item. The former model rendered the whole view
   again and diffed it: ~312µs per commit for the same view.
+- High-frequency updates (`cargo test --release -p nsh-core bench_high_frequency -- --ignored --nocapture`: `Board`, a quote board of
+  80 rows of a mark and five cells, 961 views, the first cells of 40 rows changing together on each press, their texts' lengths too):
+  ~4.4µs per event, then per commit ~140µs when the cells have a size of their own and ~180µs when they take their text's, 40 mount
+  items either way. Nearly all of the commit is Taffy laying the dirty paths out again (~116µs of it, measured once by timing the
+  commit's phases): a changed text dirties every view up to the root, and the flex algorithm of the root and of each changed row runs
+  again over all their children, fixed cells included (nothing stops the dirtying at a view whose size its content cannot change); the
+  frame walk is ~10µs, the texts (the tests' engine) under 2µs. This is the baseline relayout boundaries are measured against.
+  With relayout boundaries (`LayoutTree::is_boundary`): ~36µs per commit for the fixed cells, each changed cell laid out on its own
+  and nothing above it; content-sized cells are not boundaries (their size is their text's) and stay at ~180µs.
 - Rust components' handles (`handle_bench`: `cargo test --release -p nsh-engine --test engine bench_handles --
   --ignored --nocapture --test-threads=1`, 1000 calls in one press, the minimum of 200 presses, per call; steady
   state of five runs). In brackets, when `AnimatedValue` was an `Rc` of its node and `ElementRef` an `Rc` of its
diff --git a/third-party/nsh/docs/dev/pitfalls.md b/third-party/nsh/docs/dev/pitfalls.md
index 4aa53128b5..2cb086bc17 100644
--- a/third-party/nsh/docs/dev/pitfalls.md
+++ b/third-party/nsh/docs/dev/pitfalls.md
@@ -52,10 +52,19 @@ Things that were found the hard way. Read the section of the area you change bef
   layout could round a node from stale values (a frame one point off from a layout from scratch). The core reaches every node through
   its parent, its containing block (there is no `position: static`), and sums the origins from the root down, in Taffy's order, so the
   `f32` sums and the rounding are Taffy's to the bit (`--trace` output is unchanged).
-- **Taffy 0.15's `mark_dirty` stops at the first node whose cache is already empty**, taking its ancestors for dirty too. Nodes of a
-  `display: none` subtree always have empty caches while the hidden node above keeps its result, so a view moved in there keeps its
-  former layout: the frame walk gives everything under a `display: none` view an empty frame and its texts an empty content box,
-  whatever Taffy keeps for them (`emit_frames_in`, `flush_text`).
+- **The core keeps its own layout tree** (`core/layout_tree.rs`: Taffy's algorithms over the core's nodes through Taffy's low-level
+  traits, as `TaffyTree` runs them, call for call). `TaffyTree::mark_dirty` stops at the first node whose cache is already empty, taking
+  its ancestors for dirty too; nodes of a `display: none` subtree and absolutely positioned ones always have empty caches while the nodes
+  above keep theirs, so a view moved in there kept its former layout. `LayoutTree::mark_dirty` clears every cache up to the root, and the
+  tree keeps no hoisted children (only Taffy's rounding pass reads them). The frame walk also gives everything under a `display: none`
+  view an empty frame and its texts an empty content box (`emit_frames_in`, `flush_text`): a hidden subtree is never read.
+- **Relayout boundaries are flex items whose size nothing inside can change** (`LayoutTree::is_boundary`): fixed points both ways, a
+  flex basis of `auto`, `flexShrink: 0`, no baseline alignment. A change inside stops dirtying there and the item is laid out again with
+  what its parent gave it last (`relayout_boundaries`, before the main pass); only when what it gives back differs (overflow, say) is
+  its parent dirtied. Each condition is needed: a percentage basis resolves against a parent of no definite size as `auto` (its
+  content), and shrinking is floored at the content's automatic minimum, so either let the content change the size (the layout
+  consistency harness found the first). Growing is fine: it takes the free space the parent and siblings leave. A restyled node always
+  dirties its parent (`set_style`), a boundary included. Boundaries' frames are emitted after the passes (`commit_round`).
 - **List cells are independent Taffy roots** (not children of the list's Taffy node; their width follows the list's cross axis): scrolling only
   relays out dirty cells and only emits frames for relaid cell subtrees; when the main tree is clean, `compute_layout` and the frame walk of the
   main tree are skipped.
diff --git a/third-party/nsh/upstream.json b/third-party/nsh/upstream.json
index 58285c7393..1fd22c9289 100644
--- a/third-party/nsh/upstream.json
+++ b/third-party/nsh/upstream.json
@@ -34,11 +34,11 @@
     ".github/workflows/windows.yml": "f3ce2cef5ba8f05156459dcbeba3857c816a2871744729d7a7dd8e58079ecf6e",
     ".gitignore": "09f8ab58f988ed6ac0047bb8d497cdb6a3cc8af3af02a676f6e39a31addec242",
     ".rustfmt.toml": "3e2351e140d029ca8104ed001ae088b8041aaf2e5d9606d5ede1807a986740da",
-    "AGENTS.md": "6b2e0883676c03c9a021a67c517da2e098e67a879123bb7cd2da8c39d0fca4eb",
+    "AGENTS.md": "8f2c3d834c60111abb7389c6716caa7b2a54aaaeb177ddc4c0d1d156da196913",
     "CLAUDE.md": "336cc4fbf19beaada7ccf9986414fa91851a8d7a07dfb3ccbe800a69eed0ab49",
     "CONTRIBUTING.md": "fb27100fec01ba22dde6da1546f9cd761b8e23bec7d47eca87146c7991e85de2",
     "Cargo.lock": "f1765287f4b60baad0fe758919f878f9da61b6ac0f3e8a17ed9488b3608955c6",
-    "Cargo.toml": "fc0059d9067e1ce8c9bebb563d4e244985346c557e0c9d121e16e807be4ce9c5",
+    "Cargo.toml": "95f2a961a6236d03988ab3d4124c9d7586e9da91f775a2bf9f8e669a95e428a5",
     "LICENSE-APACHE": "ec75427ad955e3c740f22e7dc3f6db6fc5b2cc1d9135c2af91cece8dd47a2d38",
     "README.md": "bfbb302f6b0689fcfdc8d30cd1133fc11bca801677d17e517b2adf2d67b77fca",
     "crates/gpui/.cargo/config.toml": "5a1d7d3574a4c7ca0d894a26b0fd71d0fb2f42c85552af2c1539060148f2a1bc",
@@ -231,16 +231,17 @@
     "crates/nsh-core/src/color.rs": "93afe9bf38e8f7b12108fedc4863f8e1bee3a55ae7d0c75e788b79b02fb72a93",
     "crates/nsh-core/src/core/classes.rs": "242ff14438c0785b6d29af6315273d4f0256cc048fad221b4cabd2f3a0f2027f",
     "crates/nsh-core/src/core/components.rs": "93f3cb23f3a570c620d8204bf46a125885f35cebe71e56e637eaafcb673dd057",
+    "crates/nsh-core/src/core/layout_tree.rs": "dc834a3994e16fc86df87c99bb6d6a718adf30693b3e9ea5f91e5bbd3796741d",
     "crates/nsh-core/src/core/list.rs": "e709dcf029ac3d8d0c4bc87b4e1984ce07f50e18266d0759eaed773de1902684",
     "crates/nsh-core/src/core/measure.rs": "c6692abd3002576afef5b6d777d7694e28e4754bf7ada0adf8ed1c92419289c1",
-    "crates/nsh-core/src/core/mod.rs": "0dddd5ad69f1c2bf6e67fe69b0d49f8d8487e9c0b31c4f72d486cbab1e907406",
-    "crates/nsh-core/src/core/overlay.rs": "21be85fdbba3484b0e5ea2d7455f821c197bf3af195ef2532a5bdc37080987a5",
-    "crates/nsh-core/src/core/parent_viewport.rs": "7aba431169b7dbd3000896a2aa6af865b18286826ce031182c0903278ffb71b6",
+    "crates/nsh-core/src/core/mod.rs": "641f85478a333a2063fb7c2899869bbb271f8eae89b7d38322f374e5300eb00d",
+    "crates/nsh-core/src/core/overlay.rs": "7839b8e7da75b4599321d486a8891dfbd3a4514bc1992926b60fdb5b64640f29",
+    "crates/nsh-core/src/core/parent_viewport.rs": "1326e1b74de2d14cb9b0a99be4ff00b6f2509f79c6a98a3597e8508ac08d3fa6",
     "crates/nsh-core/src/core/reading.rs": "3bb3d25dc8b97f35eb73dcbaf2d6f1b5dff8cdbdd012a8edcef7fab6661f3d9f",
     "crates/nsh-core/src/core/renders.rs": "eba08379b95272762630258bba8c9d1908f8e8ec94d9518bfd59470edf49b5cd",
     "crates/nsh-core/src/core/services.rs": "8812eefd00661841ff2b1de5d4f8cf68dd667da4151b4c79ed966cc940934752",
     "crates/nsh-core/src/core/sticky.rs": "2c508038c1677fc10f11b5e6c1bf245cb78a75a02beaac24d0b09b2033a4beaa",
-    "crates/nsh-core/src/core/tests.rs": "1e4f1112be94a7cb3d86fa8242ef4a679b9e4016ee30a7ae4e4bddbb44ebc7fb",
+    "crates/nsh-core/src/core/tests.rs": "fdecbbbaee572484f54084e954911afd34f385e7f2cfb02874c82a7eca592386",
     "crates/nsh-core/src/core/touch.rs": "a8559c023a443a6defad135af2f319e60130e7df23af2a8353239b0862e494a8",
     "crates/nsh-core/src/core/views.rs": "c32c59533d5ee33bc16677b0459816f7670c224f3f8de3507c72029989660834",
     "crates/nsh-core/src/css.rs": "56760520c706f64505849d5a32a1eada2d9fd243a5c70e07f21bc8ed0105217f",
@@ -845,7 +846,7 @@
     "docs/dev/api-naming.md": "5286d04f523bd17c9ceaecb37612a145c52a91684889293b43fbf6f0da7a9363",
     "docs/dev/architecture.md": "dd96d2a3ea5d26d23d17e0db36a1b280486be933eb83452f8fbb5d4c87023951",
     "docs/dev/canvas.md": "e6c644b1198801f9242aac91339dc04b5c480d876dbcbe378c58b4953f3970c2",
-    "docs/dev/compatibility.md": "0307638db496f5ea70fd6c996ae5839336c54e370ed495b8fb06d860eb6a02bd",
+    "docs/dev/compatibility.md": "26336d6349cbedd0a70990dd5a4d2e7e48c604242974ac164e68bbeb4ad1cddd",
     "docs/dev/components.md": "9f4a41c405a016275002fc9d850a222dcf941eb47f96201a1a384ba3f8a34914",
     "docs/dev/dock.md": "51527992aeec0387fe7ebdb82b7d24ed4ab6f0dc19b3332e23236cb7a3ad1041",
     "docs/dev/elements.md": "94da584a4b2a7dfbd54c83799329ed05220ce8f2c86107df1e556148dd2b790f",
@@ -859,8 +860,8 @@
     "docs/dev/linux.md": "4441ec64b6edc5c03084bd203aa5114c066f9e2c0541ee320be8f17b6f82c3fe",
     "docs/dev/macos.md": "827c809b5774681c77daf05ac6603342c027dfd459e78f55558044861711dcd3",
     "docs/dev/native-pipeline.md": "07f803365af6ac90d5247c70d2e85ce9e03393f0e9e122c46690f85e381a0c55",
-    "docs/dev/performance.md": "c40be7a2052abf26542b024775ec10219247af26a473bb3f784c02e4f4a5209f",
-    "docs/dev/pitfalls.md": "b8dc2e12cdb09531c2903898028fa963104f13fbbee34677db37762781f13343",
+    "docs/dev/performance.md": "55a4d5a9af1098aa3d7f1ee9fbb71d188576542e539b989aacdcaffc28d140bf",
+    "docs/dev/pitfalls.md": "366400d2c07ab095536caa2907e0e5e451dcc68c2a08ccb88264cbc2fe23e24d",
     "docs/dev/reactive-components.md": "cd447fe8cfbe9026d7cadd5fa42d9c387e4c1b1b7e1c5adddd55c3f1beb94a4d",
     "docs/dev/rust-classes.md": "1f70015213dbcf56e6d6e4641019092003c22eb57ca324d9c4a85578b4468139",
     "docs/dev/rust-modules.md": "f793bfb12272ded93e80876fd2c72ff8dc0f1bd0337977f39b750af495a83592",
@@ -1673,12 +1674,52 @@
     "website/zh-CN/modules/tailwind-reference.md": "9f366de10fe05a3fcf34f7e2ad7a6d7f8dc82379084f4958bef2c5761aff968f"
   },
   "upstream_repository": "https://github.com/longbridge/nsh",
-  "upstream_revision": "de52bb3f9c236c48f0414f1bb36c17b92914ff91",
+  "upstream_revision": "111b3a933319fadb0f1da9a59909ca526f256386",
   "file_sources": {
+    "AGENTS.md": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
+    "Cargo.toml": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
     "README.md": {
       "repository": "https://github.com/longbridge/nsh",
       "revision": "de52bb3f9c236c48f0414f1bb36c17b92914ff91"
     },
+    "crates/nsh-core/src/core/layout_tree.rs": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
+    "crates/nsh-core/src/core/mod.rs": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
+    "crates/nsh-core/src/core/overlay.rs": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
+    "crates/nsh-core/src/core/parent_viewport.rs": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
+    "crates/nsh-core/src/core/tests.rs": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
+    "docs/dev/compatibility.md": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
+    "docs/dev/performance.md": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
+    "docs/dev/pitfalls.md": {
+      "repository": "https://github.com/longbridge/nsh",
+      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+    },
     "website/DESIGN.md": {
       "repository": "https://github.com/longbridge/nsh",
       "revision": "de52bb3f9c236c48f0414f1bb36c17b92914ff91"


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org