Skip to content

fix(react): clear removed snapshot prop refs#2590

Merged
Yradex merged 2 commits into
lynx-family:mainfrom
Yradex:wt/run-on-background-clear-prop-refs
May 14, 2026
Merged

fix(react): clear removed snapshot prop refs#2590
Yradex merged 2 commits into
lynx-family:mainfrom
Yradex:wt/run-on-background-clear-prop-refs

Conversation

@Yradex
Copy link
Copy Markdown
Collaborator

@Yradex Yradex commented May 9, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Cleared transient snapshot child property references when snapshot subtrees are removed, preventing deleted items from being retained.
  • Tests

    • Added tests covering cleanup behavior for removed children, including nested arrays and cyclic reference scenarios.

Review Change Stack

Summary

This fixes a main-thread snapshot retention path where removed child subtrees could still be strongly referenced by compiler-generated transient child props. Snapshot JSX uses $* props as staging references for named children; when SnapshotInstance.removeChild() detached a child, the structural tree and native elements were cleaned up, but those $* props could still point at the removed snapshot subtree.

The practical failure mode is that a deleted list holder or list item subtree can remain reachable from the root props chain after it is removed. That keeps old snapshot instances alive even though they are no longer part of the rendered tree.

Key points

  • Clear transient $* owner props during removal. Direct refs such as { $0: removedChild } are deleted from props, so the owner no longer keeps a hard reference to the removed subtree after removeChild().

  • Handle compiled array child shapes recursively. When a transient prop stores children as arrays, only removed snapshot entries are cleared, including nested arrays:

    // before removal
    owner.props.$0 = [keptChild, [removedChild]];
    
    // after removeChild(removedChild)
    owner.props.$0 = [keptChild, [undefined]];
  • Clean transient props inside the removed subtree as it is torn down. This breaks references held by the deleted subtree itself, including component/list structures that had their own compiled child staging props.

Runtime Contract

This only affects compiler-owned transient child props whose keys start with $. Normal user props, public runtime APIs, serialized snapshot shape, native patch operations, and list update protocols are unchanged.

The cleanup is tied to SnapshotInstance.removeChild(): removed snapshot instances and descendants are collected into a removal set, then $* prop values are cleared if they directly reference any removed snapshot or contain one inside an array. Existing array identity is preserved, and non-removed entries keep their current identity and order.

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).
  • Changeset added, and when a BREAKING CHANGE occurs, it needs to be clearly marked (or not required).

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 9, 2026

🦋 Changeset detected

Latest commit: 3ebe048

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@lynx-js/react Patch
@lynx-js/react-umd Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

📝 Walkthrough

Walkthrough

Adds helpers to find removed snapshot nodes and clears compiler-transient $* child prop refs during SnapshotInstance.removeChild traversal (before list-holder logic and while visiting subtree nodes). Adds tests and a changeset note documenting the behavior.

Changes

Transient child prop cleanup on snapshot removal

Layer / File(s) Summary
Cleanup helpers and removeChild integration
packages/react/runtime/src/snapshot/snapshot/snapshot.ts
Helper functions detect removed snapshot instances and clear transient $* props; removeChild computes the removed set and invokes clearing before list-holder logic and during both list-holder and non-list-holder subtree traversals.
Test coverage for transient cleanup
packages/react/runtime/__test__/snapshot/renderToOpcodes.test.jsx
Adds four tests validating single and nested transient $* prop clearing, multiple named $* keys being cleared, and cyclic transient-array tolerance; updates renderOpcodesInto expectations for cleared vnodes.
Changeset documentation
.changeset/clear-snapshot-prop-refs.md
Patch release note documenting that transient snapshot child props are cleared on subtree detachment to avoid stale compiled $* references.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • lynx-family/lynx-stack#2041: Modifies subtree removal timing for snapshotDestroyList, touching the same removal traversal where transient prop cleanup is added.
  • lynx-family/lynx-stack#2393: Adds removeChildren and refactors removal paths in the snapshot runtime; the new cleanup should apply to that bulk-removal path.

Suggested reviewers

  • HuJean
  • hzy

Poem

🐰 I hopped through props both near and far,

Found $N traces clinging like a burr,
With gentle nibble and tidy sweep,
I cleared the ghosts so trees can sleep,
Now snapshots hop on, light and sure.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(react): clear removed snapshot prop refs' directly and accurately reflects the main objective of the PR: fixing a snapshot retention bug by clearing transient $* props that reference removed snapshots.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 9, 2026

Merging this PR will degrade performance by 21.72%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 2 regressed benchmarks
✅ 79 untouched benchmarks
⏩ 26 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
002-hello-reactLynx-destroyBackground 670.3 µs 916.9 µs -26.9%
008-many-use-state-destroyBackground 8 ms 9.5 ms -16.17%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing Yradex:wt/run-on-background-clear-prop-refs (3ebe048) with main (66f6ecf)

Open in CodSpeed

Footnotes

  1. 26 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 9, 2026

React External

#1321 Bundle Size — 695.33KiB (+0.33%).

3ebe048(current) vs 66f6ecf main#1314(baseline)

Bundle metrics  Change 1 change
                 Current
#1321
     Baseline
#1314
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 40.81% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 17 17
No change  Duplicate Modules 5 5
No change  Duplicate Code 8.59% 8.59%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#1321
     Baseline
#1314
Regression  Other 695.33KiB (+0.33%) 693.04KiB

Bundle analysis reportBranch Yradex:wt/run-on-background-clea...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 9, 2026

React Example with Element Template

#473 Bundle Size — 199.83KiB (0%).

3ebe048(current) vs 66f6ecf main#466(baseline)

Bundle metrics  Change 1 change
                 Current
#473
     Baseline
#466
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 85 85
No change  Duplicate Modules 25 25
Change  Duplicate Code 39.96%(-0.03%) 39.97%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#473
     Baseline
#466
No change  IMG 145.76KiB 145.76KiB
No change  Other 54.08KiB 54.08KiB

Bundle analysis reportBranch Yradex:wt/run-on-background-clea...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 9, 2026

Web Explorer

#9781 Bundle Size — 901.38KiB (0%).

3ebe048(current) vs 66f6ecf main#9775(baseline)

Bundle metrics  Change 2 changes
                 Current
#9781
     Baseline
#9775
No change  Initial JS 45.06KiB 45.06KiB
No change  Initial CSS 2.22KiB 2.22KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 9 9
No change  Assets 11 11
Change  Modules 227(-0.44%) 228
No change  Duplicate Modules 11 11
Change  Duplicate Code 27.23%(+0.04%) 27.22%
No change  Packages 10 10
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#9781
     Baseline
#9775
No change  JS 497.1KiB 497.1KiB
No change  Other 402.06KiB 402.06KiB
No change  CSS 2.22KiB 2.22KiB

Bundle analysis reportBranch Yradex:wt/run-on-background-clea...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 9, 2026

React MTF Example

#1340 Bundle Size — 208.1KiB (+0.31%).

3ebe048(current) vs 66f6ecf main#1333(baseline)

Bundle metrics  Change 2 changes
                 Current
#1340
     Baseline
#1333
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 46.38% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 192 192
No change  Duplicate Modules 77 77
Change  Duplicate Code 44.4%(+0.05%) 44.38%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#1340
     Baseline
#1333
No change  IMG 111.23KiB 111.23KiB
Regression  Other 96.86KiB (+0.66%) 96.23KiB

Bundle analysis reportBranch Yradex:wt/run-on-background-clea...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 9, 2026

React Example

#8207 Bundle Size — 237.15KiB (+0.27%).

3ebe048(current) vs 66f6ecf main#8200(baseline)

Bundle metrics  Change 2 changes
                 Current
#8207
     Baseline
#8200
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 38.37% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 197 197
No change  Duplicate Modules 80 80
Change  Duplicate Code 44.89%(+0.04%) 44.87%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#8207
     Baseline
#8200
No change  IMG 145.76KiB 145.76KiB
Regression  Other 91.39KiB (+0.7%) 90.75KiB

Bundle analysis reportBranch Yradex:wt/run-on-background-clea...Project dashboard


Generated by RelativeCIDocumentationReport issue

@Yradex Yradex force-pushed the wt/run-on-background-clear-prop-refs branch from d81fa3c to 2354303 Compare May 9, 2026 14:07
@Yradex Yradex marked this pull request as ready for review May 12, 2026 06:34
@Yradex Yradex requested review from HuJean and hzy as code owners May 12, 2026 06:34
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react/runtime/src/snapshot/snapshot/snapshot.ts`:
- Around line 74-83: clearTransientChildPropRefs currently retraverses the
removed subtree via traverseSnapshotInstance, causing O(n²) behavior when
callers repeatedly call clearTransientChildPropRefs(v, v); refactor to accept a
precomputed WeakSet of removed snapshots (e.g., add an optional parameter
removedSnapshots: WeakSet<object>) or an overload so callers can compute
removedSnapshots once using traverseSnapshotInstance(removedChild, ...) and pass
it in; update clearTransientChildPropRefs(owner, removedChild,
removedSnapshots?) to use the passed set if present (fall back to current
traversal only if not provided) and change the current call sites that do
clearTransientChildPropRefs(v, v) to compute the removedSnapshots once and pass
it in, preserving existing behavior when not provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bec8ee41-a6e0-4765-a7a9-26704fb7ebee

📥 Commits

Reviewing files that changed from the base of the PR and between 87c1759 and 2354303.

📒 Files selected for processing (3)
  • .changeset/clear-snapshot-prop-refs.md
  • packages/react/runtime/__test__/snapshot/renderToOpcodes.test.jsx
  • packages/react/runtime/src/snapshot/snapshot/snapshot.ts

Comment thread packages/react/runtime/src/snapshot/snapshot/snapshot.ts Outdated
Comment thread packages/react/runtime/src/snapshot/snapshot/snapshot.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/react/runtime/__test__/snapshot/renderToOpcodes.test.jsx (1)

111-122: 💤 Low value

Consider testing deeper nesting levels.

The test verifies 2-level nesting ([[child]]), which is good. Since the PR description mentions recursive handling of array shapes, consider adding a test case with 3+ levels of nesting (e.g., [[[child]]]) to more thoroughly verify the recursive cleanup logic handles arbitrary depth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/runtime/__test__/snapshot/renderToOpcodes.test.jsx` around
lines 111 - 122, Add a parallel test that exercises deeper nesting (e.g., use
createElement('view', { $0: [[[child]]] }) and run renderToString(vnode, new
SnapshotInstance('root'))), then call vnode.removeChild(child) and assert that
vnode.props.$0 becomes the same shape with the child replaced by undefined
(e.g., [[[undefined]]]) and vnode.childNodes is empty; mimic the existing test
block ("should clear removed children from nested transient prop arrays") but
with three or more nested array levels to verify recursive cleanup in
removeChild and the transient-props handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/react/runtime/__test__/snapshot/renderToOpcodes.test.jsx`:
- Around line 111-122: Add a parallel test that exercises deeper nesting (e.g.,
use createElement('view', { $0: [[[child]]] }) and run renderToString(vnode, new
SnapshotInstance('root'))), then call vnode.removeChild(child) and assert that
vnode.props.$0 becomes the same shape with the child replaced by undefined
(e.g., [[[undefined]]]) and vnode.childNodes is empty; mimic the existing test
block ("should clear removed children from nested transient prop arrays") but
with three or more nested array levels to verify recursive cleanup in
removeChild and the transient-props handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 37d18710-3f8c-4aa0-96f4-8d5b5ba0c8db

📥 Commits

Reviewing files that changed from the base of the PR and between 2354303 and 3ebe048.

📒 Files selected for processing (2)
  • packages/react/runtime/__test__/snapshot/renderToOpcodes.test.jsx
  • packages/react/runtime/src/snapshot/snapshot/snapshot.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react/runtime/src/snapshot/snapshot/snapshot.ts

@Yradex Yradex merged commit d182a06 into lynx-family:main May 14, 2026
80 of 85 checks passed
@Yradex Yradex deleted the wt/run-on-background-clear-prop-refs branch May 14, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants