Make findComponentRoot faster with more nodeCache
This is an alternative, less-invasive, fix for #891. Test Plan: On http://jsbin.com/OqOJidIQ/2/edit, got timings like [75, 56, 30, 36, 27, 27, 28, 32, 27, 27, 28, 31] instead of the old [75, 729, 46, 32, 28, 34, 26, 27, 27, 30, 26, 26]. I also added a counter to getID and saw it was called 3014 times instead of the old 636264. (3014 is the number of nodes (3000) plus 3 calls that happen for the initial render and 1 for each of the 11 renders after that.)
Ben Alpert committed
Jan 14, 2014 at 17:44 UTC
d96c6914c7ed4efdabea8edc11d0c8ba64acb42b
2 files changed
+77
-24
src/core/ReactInstanceHandles.js
+21
-3
@@ -168,7 +168,8 @@ function getFirstCommonAncestorID(oneID, twoID) {
168
169
/**
170
* Traverses the parent path between two IDs (either up or down). The IDs must
171
- * not be the same, and there must exist a parent path between them.
171
+ * not be the same, and there must exist a parent path between them. If the
172
+ * callback returns `false`, traversal is stopped.
173
*
174
* @param {?string} start ID at which to start traversal.
175
* @param {?string} stop ID at which to end traversal.
@@ -197,10 +198,11 @@ function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
198
var depth = 0;
199
var traverse = traverseUp ? getParentID : getNextDescendantID;
200
for (var id = start; /* until break */; id = traverse(id, stop)) {
201
+ var ret;
202
if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
201
- cb(id, traverseUp, arg);
203
+ ret = cb(id, traverseUp, arg);
204
}
203
- if (id === stop) {
205
+ if (ret === false || id === stop) {
206
// Only break //after// visiting `stop`.
207
break;
208
}
@@ -296,6 +298,22 @@ var ReactInstanceHandles = {
298
}
299
},
300
301
+ /**
302
+ * Traverse a node ID, calling the supplied `cb` for each ancestor ID. For
303
+ * example, passing `.r[0].{row-0}.[1]` would result in `cb` getting called
304
+ * with `.r[0]`, `.r[0].{row-0}`, and `.r[0].{row-0}.[1]`.
305
+ *
306
+ * NOTE: This traversal happens on IDs without touching the DOM.
307
+ *
308
+ * @param {string} targetID ID of the target node.
309
+ * @param {function} cb Callback to invoke.
310
+ * @param {*} arg Argument to invoke the callback with.
311
+ * @internal
312
+ */
313
+ traverseAncestors: function(targetID, cb, arg) {
314
+ traverseParentPath('', targetID, cb, arg, true, false);
315
+ },
316
+
317
/**
318
* Exposed for unit testing.
319
* @private
src/core/ReactMount.js
+56
-21
@@ -163,6 +163,33 @@ function purgeID(id) {
163
delete nodeCache[id];
164
}
165
166
+var deepestNodeSoFar = null;
167
+function findDeepestCachedAncestorImpl(ancestorID) {
168
+ var ancestor = nodeCache[ancestorID];
169
+ if (ancestor && isValid(ancestor, ancestorID)) {
170
+ deepestNodeSoFar = ancestor;
171
+ } else {
172
+ // This node isn't populated in the cache, so presumably none of its
173
+ // descendants are. Break out of the loop.
174
+ return false;
175
+ }
176
+}
177
+
178
+/**
179
+ * Return the deepest cached node whose ID is a prefix of `targetID`.
180
+ */
181
+function findDeepestCachedAncestor(targetID) {
182
+ deepestNodeSoFar = null;
183
+ ReactInstanceHandles.traverseAncestors(
184
+ targetID,
185
+ findDeepestCachedAncestorImpl
186
+ );
187
+
188
+ var foundNode = deepestNodeSoFar;
189
+ deepestNodeSoFar = null;
190
+ return foundNode;
191
+}
192
+
193
/**
194
* Mounting is the process of initializing a React component by creatings its
195
* representative DOM elements and inserting them into a supplied `container`.
@@ -502,46 +529,45 @@ var ReactMount = {
529
},
530
531
/**
505
- * Finds a node with the supplied `id` inside of the supplied `ancestorNode`.
506
- * Exploits the ID naming scheme to perform the search quickly.
532
+ * Finds a node with the supplied `targetID` inside of the supplied
533
+ * `ancestorNode`. Exploits the ID naming scheme to perform the search
534
+ * quickly.
535
*
536
* @param {DOMEventTarget} ancestorNode Search from this root.
509
- * @pararm {string} id ID of the DOM representation of the component.
510
- * @return {DOMEventTarget} DOM node with the supplied `id`.
537
+ * @pararm {string} targetID ID of the DOM representation of the component.
538
+ * @return {DOMEventTarget} DOM node with the supplied `targetID`.
539
* @internal
540
*/
513
- findComponentRoot: function(ancestorNode, id) {
541
+ findComponentRoot: function(ancestorNode, targetID) {
542
var firstChildren = findComponentRootReusableArray;
543
var childIndex = 0;
544
517
- firstChildren[0] = ancestorNode.firstChild;
545
+ var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode;
546
+
547
+ firstChildren[0] = deepestAncestor.firstChild;
548
firstChildren.length = 1;
549
550
while (childIndex < firstChildren.length) {
551
var child = firstChildren[childIndex++];
552
+ var targetChild;
553
+
554
while (child) {
555
var childID = ReactMount.getID(child);
556
if (childID) {
525
- if (id === childID) {
526
- // Emptying firstChildren/findComponentRootReusableArray is
527
- // not necessary for correctness, but it helps the GC reclaim
528
- // any nodes that were left at the end of the search.
529
- firstChildren.length = 0;
530
-
531
- return child;
532
- }
533
-
534
- if (ReactInstanceHandles.isAncestorIDOf(childID, id)) {
557
+ // Even if we find the node we're looking for, we finish looping
558
+ // through its siblings to ensure they're cached so that we don't have
559
+ // to revisit this node again. Otherwise, we make n^2 calls to getID
560
+ // when visiting the many children of a single node in order.
561
+
562
+ if (targetID === childID) {
563
+ targetChild = child;
564
+ } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) {
565
// If we find a child whose ID is an ancestor of the given ID,
566
// then we can be sure that we only want to search the subtree
567
// rooted at this child, so we can throw out the rest of the
568
// search state.
569
firstChildren.length = childIndex = 0;
570
firstChildren.push(child.firstChild);
541
-
542
- // Ignore the rest of this child's siblings and immediately
543
- // continue the outer loop with child.firstChild as child.
544
- break;
571
}
572
573
} else {
@@ -555,6 +581,15 @@ var ReactMount = {
581
582
child = child.nextSibling;
583
}
584
+
585
+ if (targetChild) {
586
+ // Emptying firstChildren/findComponentRootReusableArray is
587
+ // not necessary for correctness, but it helps the GC reclaim
588
+ // any nodes that were left at the end of the search.
589
+ firstChildren.length = 0;
590
+
591
+ return targetChild;
592
+ }
593
}
594
595
firstChildren.length = 0;
@@ -564,7 +599,7 @@ var ReactMount = {
599
'findComponentRoot(..., %s): Unable to find element. This probably ' +
600
'means the DOM was unexpectedly mutated (e.g., by the browser). ' +
601
'Try inspecting the child nodes of the element with React ID `%s`.',
567
- id,
602
+ targetID,
603
ReactMount.getID(ancestorNode)
604
);
605
},