Skip to content

fix(helpers): guard addChild against re-inserting an already-linked node#1163

Open
terminalchai wants to merge 1 commit into
juliangarnier:masterfrom
terminalchai:fix/addchild-self-referential-node
Open

fix(helpers): guard addChild against re-inserting an already-linked node#1163
terminalchai wants to merge 1 commit into
juliangarnier:masterfrom
terminalchai:fix/addchild-self-referential-node

Conversation

@terminalchai

Copy link
Copy Markdown

Problem

Closes #1138

When overlapping replace tweens are created on the same target/property in rapid succession (e.g. promise callbacks racing with document.hidden foreground events), addChild can be called with a tween that is already present in the composition replace list. This produces a self-referential linked list cycle (tween._prevRep === tween) that freezes the animation engine update loop, causing a tab hang.

The issue was observed in devtools as the same tween ID repeating thousands of times in the _prevRep chain.

Root cause

addChild inserts unconditionally. If child is already in the list its _prevRep/_nextRep pointers are non-null, so the sorted-walk while (prev && sortMethod && ...) will eventually reach the child itself as prev. At that point:

prev[nextProp] = child  // child._nextRep = child  ← cycle!

Fix

Add an early guard at the top of addChild that detects whether the child is already linked and calls removeChild first:

if (parent._head === child || child[prevProp] != null) {
  removeChild(parent, child, prevProp, nextProp);
}
  • parent._head === child — child is the head of the list (its _prevRep is null but it is already inserted)
  • child[prevProp] != null — child has a predecessor, meaning it is mid-list or tail

removeChild cleanly unlinks the node (setting both pointers to null) before addChild re-inserts it at the correct sorted position. When called with the default _prev/_next props this also protects the main engine ticking list against the same edge case.

Files changed

  • src/core/helpers.js — source fix
  • dist/ — rebuilt

Re-adding a tween that is already present in the composition replace list
causes addChild to produce a self-referential linked list cycle
(child._nextRep === child). The sorted-walk inside addChild can land on
the child itself as prev, then sets prev[nextProp] = child which
makes the child point to itself, freezing the animation engine update loop.

Fix: at the top of addChild, detect whether the child is already linked
in this list (parent._head === child, meaning it is the head, or
child[prevProp] != null, meaning it has a predecessor). When detected,
call removeChild first so the node is cleanly unlinked before being
re-inserted at its new sorted position.

The default value of _prevRep / _nextRep on a fresh tween is undefined,
so the != null guard (loose inequality) correctly ignores unlinked nodes
while still catching both null (after a previous removeChild) and an
object reference (still linked).

Fixes juliangarnier#1138
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.

[BUG] addChild can create self‑referential _prevRep / _nextRep when re‑adding an already‑linked tween

1 participant