Skip to content

Isolate scroll-driven virtualization updates - #4972

Open
marcus-pousette wants to merge 5 commits into
cybersemics:mainfrom
marcus-pousette:perf/isolate-scroll-virtualization-updates
Open

Isolate scroll-driven virtualization updates#4972
marcus-pousette wants to merge 5 commits into
cybersemics:mainfrom
marcus-pousette:perf/isolate-scroll-virtualization-updates

Conversation

@marcus-pousette

@marcus-pousette marcus-pousette commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #4969.

Problem

LayoutTree subscribes to a computed viewportBottom derived from scrollTop. Every scroll update therefore rerenders LayoutTree, TransitionGroup, and the full thought list even when no thought crosses the virtualization boundary.

Exact differential testing also exposed a related bookkeeping dependency: useSizeTracking deleted unmounted size entries by mutating the existing map. The old scroll-wide rerender eventually observed that mutation. Once the unnecessary parent rerender was removed, reverse virtualization could retain stale document height.

Solution

  • Move the scroll subscription into TreeNode and select only the boolean isBelowViewport.
  • Pass stable viewport-height and boundary-offset values from LayoutTree.
  • Remove unmounted size entries immutably so LayoutTree updates when—and only when—its layout inputs actually change.

Most scroll updates leave every node boolean unchanged, so they do not propagate through LayoutTree, TransitionGroup, or the full thought list. Nodes still update when they enter or leave the virtualized region.

The virtualization cutoff and visible UI are unchanged. Size cleanup is now immediate instead of relying on a later incidental render, which also removes transient phantom scroll space.

Correctness validation

The regression coverage exercises the real browser scroll-event path and verifies:

  • Safari-style negative elastic overscroll remains clamped at the top;
  • a 1px scroll that crosses no boundary does not commit the app;
  • strict boundary equality keeps the row at the cutoff mounted and hides the next row;
  • viewport expansion and contraction use the real resize-event path and mount or unmount the exact next row;
  • removing one measured size preserves unrelated entries, while removing an absent key preserves map identity;
  • distant thoughts still mount and unmount across a reachable large scroll; and
  • a variable-height browser round trip restores the original document height after the wrapped row is virtualized again.

A focused Puppeteer regression uses real wheel input to mount and measure a wrapped thought, then returns to the top and waits for it to be virtualized again. On the original exact PR base (b6902cf3), document height stayed stale at 1776 → 1875 → 1875px; this PR restores it to 1776 → 1875 → 1776px and passes the final equality assertion.

The production differential compared exact mounted thought values, ordering, editables, shims, bounds, transforms, opacity, breadcrumbs, nav/footer geometry, scroll height, and screenshots:

  • A 150-level progressively navigated path matched at ten navigation checkpoints and all 76 post-settle scroll positions. Stable screenshots were pixel-identical after excluding the expected build/TSID footer metadata.
  • A variable-height 180-sibling list matched mounted values, counts, and bounds through 81 forward and 81 reverse positions. The initial patch retained 972px of stale height after returning to the top; the immutable cleanup fixes that regression and returns exactly to the initial height. The baseline itself needs one additional scroll render to discard its final stale measurement.

On an iPhone 13 mini running iOS 26.6, five complete top-to-bottom-to-top cycles of the 180-sibling fixture repeated the same top/bottom scroll heights and mounted counts. The WebContent PID and performance.timeOrigin stayed constant, no runtime error occurred, and the historical mount/unmount loop did not reproduce.

Performance

The production Chrome workload imports a 150-deep path, sets the final cursor directly, lets the layout settle, and scrolls through 75 distinct positions. The Chrome results below were rerun after updating this branch through upstream ebdfdef83, so both variants include the related VirtualThought cleanup from #4975.

Environment Metric Current main This PR Change
Production Chrome, N=5 Task duration 911.1 ms 160.6 ms -82.4%
Production Chrome, N=5 Script duration 661.8 ms 26.5 ms -96.0%
Production Chrome, N=5 Renderer CPU 1387.1 ms 275.2 ms -80.2%
Production Chrome, N=5 Layout duration 20.1 ms 0 ms -100%
Earlier iPhone 13 mini, N=3 Average WebContent CPU 46.68% 40.07% -14.2%

Chrome samples used fresh production-browser processes, one discarded warmup per variant, alternating run order, and no React hook, geometry monkeypatch, or long-task instrumentation. Task rMAD was 1.83%/1.67% and renderer-CPU rMAD was 1.78%/0.92% for baseline/candidate.

A separate instrumented run confirmed that React commits, LayoutTree renders, and nav/footer geometry reads during the 75-step deep-path scroll all dropped to zero. Both variants retained identical aggregate DOM/listener counts, editable counts, scroll range, and sampled positions; the exact differential above separately verifies node identity and visual/layout semantics.

The physical result is corroboration from one iPhone 13 mini, not a general iOS benchmark. It predates the latest upstream-main refresh; the #4972 runtime files are unchanged, but the combined updated head was not rerun on the device. This synthetic workload isolates post-settle scrolling; it does not measure progressive-navigation speed or animation quality. No memory improvement is claimed.

In a prior candidate-only depth-scaling run, depth 500 completed the same scroll workload with 413.2 ms task time and 644.9 ms renderer CPU. This confirms remaining depth-dependent selector cost, but the increase was sub-proportional to the 3.33x increase in depth.

I also remeasured the opposite stress case on current main: 180 variable-height siblings, scrolling fully down and back so rows repeatedly cross the virtualization boundary. Across six balanced alternating production runs, renderer CPU increased 6.3% (3233.7 to 3437.4 ms), task time increased 4.2% (2071.3 to 2158.7 ms), and layout time increased from 70.0 to 90.2 ms, while script time decreased 4.3%. Every run matched the initial/final DOM shape, 7229px document height, 6565px scroll range, 150 positions, and final scroll position 0. This workload deliberately maximizes mount/unmount churn. The extra layout work comes from immediately publishing removed measurements; omitting it is faster but leaves stale document height, as the differential test above demonstrated. The optimization therefore strongly improves deep-path scrolling without claiming a universal win for every scroll shape.

Verification

  • yarn test --run src/hooks/__tests__/useSizeTracking.ts src/components/__tests__/LayoutTree.virtualization.ts src/components/__tests__/LayoutTree.ts
  • focused Puppeteer hidden-shim cursor tests
  • variable-height Puppeteer virtualization regression (red on the original exact PR base, green on this branch)
  • yarn lint:tsc --pretty false
  • yarn lint:src
  • yarn build
  • production Chrome exact semantic/visual A/B
  • updated-current-main deep N=5 and wide-boundary N=6 production A/B
  • physical iPhone wide-boundary soak

@marcus-pousette
marcus-pousette force-pushed the perf/isolate-scroll-virtualization-updates branch 2 times, most recently from 7eefba1 to efa27de Compare August 17, 2026 18:45
@marcus-pousette
marcus-pousette force-pushed the perf/isolate-scroll-virtualization-updates branch from efa27de to 13a6a62 Compare August 17, 2026 18:48
@marcus-pousette
marcus-pousette force-pushed the perf/isolate-scroll-virtualization-updates branch from 13a6a62 to a6586e7 Compare August 17, 2026 18:49
@raineorshine

Copy link
Copy Markdown
Contributor

Thanks for this.

@trevinhofmann FYI This has direct consequences for #4735.

marcus-pousette and others added 2 commits August 17, 2026 21:10
Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>
Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>
Exercise Safari elastic overscroll and the real viewport resize path, verify size-map cleanup semantics, and cover document-height restoration with real browser scrolling.

Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>
Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>
@marcus-pousette

marcus-pousette commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

(Below written by AI and some minor modifications by me)

Additional physical-device validation:

I reran the exact pre-/post-patch comparison using a 150-level Hello1Hello150 fixture and production Forward/Back commands through Hello1 → Hello100 → Hello1. Each run used the same requested scroll sequence, started behind a cooling gate (I measured the temperatures of my phones to verify that they where not heating up excessively and affect perf ! ), and verified cursor depth/value, breadcrumbs, visibility, final state, page identity, and runtime errors.

Galaxy S22 / Chrome 150

Three paired PRE/POST blocks completed successfully with no retries. Every sample executed 99 Forward + 99 Back commands and 396 controlled scroll calls.

Metric Median POST change
Renderer CPU -10.40%
Task duration -13.63%
Script duration -27.73%
Layout duration -16.78%
rAF gaps over 50ms -16.17%
Long-task count -9.05%

All three pairs favored the PR. Idle controls did not show a comparable CPU advantage. Memory endpoints were essentially unchanged, so no memory improvement is claimed.

iPhone 13 mini / Mobile Safari

All seven retained app workloads completed the full navigation and returned to Hello1 without a reload, root reset, blank screen, runtime error, or geometry failure.

However monitoring of Safari was problematic for some reason and doing many runs only bench gave interpretable results:

  • Raw WebContent CPU time: -2.73%
  • Mean WebContent CPU: -2.87%
  • Idle-adjusted CPU: +1.66%
  • rAF p95: 21ms → 19–20ms

One pair is insufficient and was ordered POST→PRE, so the iPhone result is inconclusive. This supersedes the earlier iPhone N=3 / -14.2% figure.

Overall, the added testing provides strong evidence of a targeted deep-tree performance improvement on Android, while making no broad cross-device or memory-performance claim.

Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants