@samitouri / QOS-React-2 / commits / d96c6914c7

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 },