Skip to content

Commit 642aed0

Browse files
authored
Merge pull request #5183 from preactjs/perf/v11-retained-subtree-bailout
Avoid traversing retained subtrees
2 parents 96ae724 + 8325d96 commit 642aed0

3 files changed

Lines changed: 68 additions & 17 deletions

File tree

src/diff/children.js

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -126,21 +126,19 @@ export function diffChildren(
126126
firstChildDom = newDom;
127127
}
128128

129-
let shouldPlace = childVNode._flags & INSERT_VNODE;
130-
if (shouldPlace || oldVNode._children === childVNode._children) {
129+
if (childVNode._flags & INSERT_VNODE) {
131130
oldDom = insert(
132131
childVNode,
133132
oldDom,
134133
parentDom,
135-
shouldPlace,
136134
oldVNode._original == NULL
137135
);
138136

139137
// When a matched VNode is physically moved via INSERT_VNODE, its old
140138
// _dom pointer becomes a stale positional reference. Clear it so that
141139
// getDomSibling (called from nested diffs) won't return this stale
142140
// reference and mis-place subsequent DOM nodes. See #5065.
143-
if (shouldPlace && oldVNode._dom) {
141+
if (oldVNode._dom) {
144142
oldVNode._dom = NULL;
145143
}
146144
} else if (typeof childVNode.type == 'function' && result !== UNDEFINED) {
@@ -351,11 +349,10 @@ function constructNewChildrenArray(
351349
* @param {VNode} parentVNode
352350
* @param {PreactElement} oldDom
353351
* @param {PreactElement} parentDom
354-
* @param {number} shouldPlace
355352
* @param {boolean} isMounting
356353
* @returns {PreactElement}
357354
*/
358-
function insert(parentVNode, oldDom, parentDom, shouldPlace, isMounting) {
355+
function insert(parentVNode, oldDom, parentDom, isMounting) {
359356
// Note: VNodes in nested suspended trees may be missing _children.
360357
if (typeof parentVNode.type == 'function') {
361358
// Root children live in another container, they never move with the
@@ -369,23 +366,21 @@ function insert(parentVNode, oldDom, parentDom, shouldPlace, isMounting) {
369366
// children's _parent pointer to point to the newVNode (parentVNode
370367
// here).
371368
children[i]._parent = parentVNode;
372-
oldDom = insert(children[i], oldDom, parentDom, shouldPlace, false);
369+
oldDom = insert(children[i], oldDom, parentDom, false);
373370
}
374371
}
375372

376373
return oldDom;
377374
} else if (parentVNode._dom != oldDom) {
378-
if (shouldPlace) {
379-
if (oldDom && parentVNode.type && !oldDom.parentNode) {
380-
oldDom = getDomSibling(parentVNode);
381-
}
375+
if (oldDom && parentVNode.type && !oldDom.parentNode) {
376+
oldDom = getDomSibling(parentVNode);
377+
}
382378

383-
if (HAS_MOVE_BEFORE_SUPPORT && !isMounting) {
384-
// @ts-expect-error This isn't added to TypeScript lib.d.ts yet
385-
parentDom.moveBefore(parentVNode._dom, oldDom);
386-
} else {
387-
parentDom.insertBefore(parentVNode._dom, oldDom || NULL);
388-
}
379+
if (HAS_MOVE_BEFORE_SUPPORT && !isMounting) {
380+
// @ts-expect-error This isn't added to TypeScript lib.d.ts yet
381+
parentDom.moveBefore(parentVNode._dom, oldDom);
382+
} else {
383+
parentDom.insertBefore(parentVNode._dom, oldDom || NULL);
389384
}
390385
oldDom = parentVNode._dom;
391386
}

src/diff/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ export function diff(
232232
commitQueue.push(c);
233233
}
234234

235+
// Skip over the retained subtree without traversing it; the
236+
// `result` branch in diffChildren picks this up as the next
237+
// oldDom.
238+
oldDom = getDomSibling(oldVNode);
239+
235240
break outer;
236241
}
237242

test/browser/lifecycles/shouldComponentUpdate.test.jsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,57 @@ describe('Lifecycle methods', () => {
8888
expect(ShouldNot.prototype.render).toHaveBeenCalledOnce();
8989
});
9090

91+
it('should not traverse a retained subtree after bailing out', () => {
92+
const Inner = () => (
93+
<Fragment>
94+
<span>A</span>
95+
<span>B</span>
96+
</Fragment>
97+
);
98+
99+
class Bailout extends Component {
100+
shouldComponentUpdate() {
101+
return false;
102+
}
103+
104+
render() {
105+
return <Inner />;
106+
}
107+
}
108+
109+
render(
110+
<div>
111+
<Bailout value={0} />
112+
<span>C</span>
113+
</div>,
114+
scratch
115+
);
116+
117+
const root = scratch._children;
118+
const bailoutVNode = root._children[0]._children[0];
119+
const innerVNode = bailoutVNode._children[0];
120+
const children = innerVNode._children;
121+
let reads = 0;
122+
bailoutVNode._children[0] = new Proxy(innerVNode, {
123+
get(target, property, receiver) {
124+
const value = Reflect.get(target, property, receiver);
125+
if (value === children) reads++;
126+
return value;
127+
}
128+
});
129+
130+
render(
131+
<div>
132+
<Bailout value={1} />
133+
<span>C</span>
134+
</div>,
135+
scratch
136+
);
137+
138+
expect(scratch.textContent).to.equal('ABC');
139+
expect(reads).to.equal(0);
140+
});
141+
91142
it('should reorder non-updating text children', () => {
92143
const rows = [
93144
{ id: '1', a: 5, b: 100 },

0 commit comments

Comments
 (0)