Intern shared styles and skip unchanged updates - #14
Conversation
StyleDesc is 1272 bytes even when most fields are unset. Long lists that reuse the same JS style object used to clone that struct onto every node. Rust now keeps a renderer-lifetime intern table. JS can send internStyle once, then attach many nodes with setStyleId. Nodes store Arc<StyleDesc>, so 20 row wrappers share one allocation. Intern ids are not reused while the renderer is live. A conflicting internStyle or an unknown setStyleId fails before any batch mutation applies. Hide, destroy, and later reuse of the same style object still resolve. Session: ses_fcd734a97ffeCh4hPdoJNAd9qV
commitUpdate always sent setStyle, including empty objects on every text node. That made chrome updates rewrite styles that did not change. The reconciler now skips a style when the object is the same reference or the fields match. Shared style constants are interned once and attached with setStyleId. Instance no longer keeps a copy of props. Refs expose id and type. Mutating a style object in place is not supported; pass a new object. Session: ses_fcd734a97ffeCh4hPdoJNAd9qV
Shared style objects now cross FFI once. Later commits skip styles that did not change. This cuts batch size and retained copies for long lists that reuse style constants. Session: ses_fcd734a97ffeCh4hPdoJNAd9qV
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad1635bc13
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| function resetStyleIntern(): void { | ||
| styleIntern = new WeakMap() | ||
| nextStyleInternId = 1 |
There was a problem hiding this comment.
Preserve intern IDs across hot remounts
When render() remounts on the same native host—the supported bun --hot path—createRoot() creates a new batching proxy, so this reset starts IDs again at 1 even though the Rust RetainedTree retains its previous intern table. If a style changes across the save, internStyle(1, newStyle) conflicts with the old entry and aborts the remount batch; associate the cache/counter with the underlying renderer or clear its native intern table instead.
AGENTS.md reference: AGENTS.md:L581-L581
Useful? React with 👍 / 👎.
| const styleId = nextStyleInternId++ | ||
| styleIntern.set(style, styleId) | ||
| getRenderer().internStyle(styleId, style) |
There was a problem hiding this comment.
Avoid permanently interning one-off dynamic styles
When a component drives a changing inline style, such as style={{ width: dragX }}, every update whose fields differ allocates a fresh ID here. The corresponding Rust map never removes entries—even after the element or JS object is collected—so a continuously updated element grows native memory without bound for the renderer's lifetime; only genuinely shared styles should be interned, or unused IDs need reclamation.
Useful? React with 👍 / 👎.
Took the intern/skip-style work off main so it can land after a real profile.
What this does
internStyle) and attach withsetStyleIdArc<StyleDesc>instead of a cloned 1272-byte structsetStylewhen the object or fields did not changesetStyleIdfails before any batch mutationWhy it is a PR
No profile yet. Skip-unchanged should cut chrome-update FFI. Intern should cut first-batch parse and retained copies when many nodes share one object. Chat already windows rows, so intern is not a 10k-row RAM win there.
Check
packages/reactstyle-updates + styles + events + virtual-listexamples/chat.test.tsx9/9launch()ofchat.tsx, sidebar open/collapse screenshotsDo not mutate a style object in place. Pass a new object.