Skip to content

Cleanroom reimplementation of the renderer tree walking - #49

Merged
maartenbreddels merged 2 commits into
masterfrom
cleanroom/reconciler
Jul 3, 2026
Merged

Cleanroom reimplementation of the renderer tree walking#49
maartenbreddels merged 2 commits into
masterfrom
cleanroom/reconciler

Conversation

@maartenbreddels

@maartenbreddels maartenbreddels commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What

Adds a faster renderer as an opt-in, off-by-default variant. The default _RenderContext keeps the original tree-walking unchanged (byte-for-byte with master); a _RenderContextFast(_RenderContext) subclass overrides only the tree-walking methods (_render, _reconsolidate, _remove_element, _visit_children, _visit_children_values). Select it with the env var REACTON_FAST=1.

CI runs the full suite against both renderers (a reacton-fast: ["0", "1"] matrix dimension on every Python version), so the two stay behavior-identical.

Why

With the default renderer, any state update pays a full tree walk — a single-leaf update, a memoized-subtree "skip", and a full force_update all cost the same ~14ms on a 300-row tree — and stale-element removal is accidentally quadratic.

How (the fast variant)

  • State setters mark needs_render_descendant up the parent chain, so a render pass only descends into subtrees that can contain work. A component subtree whose element is identical to the previous render, fully reconciled, and free of dirty/excepted contexts is skipped in both phases and keeps its previous widgets (clean_subtree).
  • force_update()/update()/first render walk fully (rc._walk_all).
  • Stale-element removal runs once per component context instead of per element.
  • Widget updates are skipped when an identical element reconciles to identical child widgets.
  • Side-effect (Layout/Style) widget tracking moved to ipywidgets' on_widget_constructed hook (shared by both renderers) instead of a per-creation global-dict diff that was O(live widgets).

Results (300-row tree, REACTON_FAST=1 vs default)

scenario default fast speedup
single leaf update 13.7 ms 0.62 ms 22x
memoized subtree skip 14.4 ms 1.0 ms 14x
root update / list reorder / force_update ~14–15 ms ~6–7 ms ~2.1x
initial render 223 ms 191 ms 1.2x

Harness (bench.py/compare.py), result JSONs, the full renderer contract both implementations satisfy, the deliberate test-invisible deltas, and known issues found during the work are documented in benchmarks/README.md.

Verification

  • Default renderer: pytest reacton/ --ignore=reacton/generate_test.py167 passed, 4 skipped.
  • Fast renderer: REACTON_FAST=1 pytest …same (167 passed, 4 skipped).
  • The default _RenderContext's five tree-walking methods are byte-for-byte identical to master (AST-verified), so the default path carries zero behavioral risk; the fast path is exercised entirely by the opt-in CI matrix dimension.

🤖 Generated with Claude Code

maartenbreddels and others added 2 commits June 15, 2026 17:51
The default renderer is unchanged: _RenderContext keeps the original
tree-walking (_render/_reconsolidate/_remove_element/_visit_children*),
byte-for-byte. A _RenderContextFast(_RenderContext) subclass overrides only
those methods with a reimplementation that does work proportional to what
changed instead of a full tree walk:

- state setters mark needs_render_descendant up the parent chain, so a render
  pass only descends into subtrees that can contain work; an identical,
  fully-reconciled, exception-free subtree is skipped in both phases
  (clean_subtree) and keeps its widgets;
- force_update/update/first render walk fully (rc._walk_all);
- stale-element removal runs once per context instead of an O(n) set-diff per
  element;
- widget updates are skipped when an identical element reconciles to identical
  child widgets.

Selected with REACTON_FAST=1 (via _render_context_class()); off by default.
Side-effect (Layout/Style) widget tracking also moved to ipywidgets'
on_widget_constructed hook (shared by both renderers) instead of a per-creation
global-dict diff that was O(live widgets).

benchmarks/ holds the harness, baseline-vs-fast result JSONs, and README.md
documenting the design, the renderer contract both implementations satisfy, the
deliberate test-invisible deltas, and known issues found during the work. On a
300-row tree the fast renderer takes a single-leaf update from 13.7ms to 0.62ms
(22x) and a memoized-subtree skip from 14.4ms to 1.0ms (14x), behavior-identical
to the default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a reacton-fast: ["0", "1"] matrix dimension so the suite runs with the
default renderer and with the opt-in fast one (REACTON_FAST=1) on every Python
version, keeping the two behavior-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@Ben-Epstein Ben-Epstein left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REACTON_FAST 🧑‍🍳

@maartenbreddels
maartenbreddels merged commit e755719 into master Jul 3, 2026
22 checks passed
maartenbreddels added a commit that referenced this pull request Jul 5, 2026
Every new build fails config validation since the deprecation; the last
green docs builds (#49/#51) predate it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants