@samitouri / QOS-React-2 / commits / 263ca4792e

Perf: Insert nodes top-down in IE and Edge

Ben Alpert committed Oct 22, 2015 at 20:58 UTC 263ca4792e250a107d09768c91a8baafb8e30087
6 files changed +142 -35
src/renderers/dom/client/ReactMount.js
+2 -1
@@ -12,6 +12,7 @@
12 'use strict';
13
14 var ClientReactRootIndex = require('ClientReactRootIndex');
15 +var DOMLazyTree = require('DOMLazyTree');
16 var DOMProperty = require('DOMProperty');
17 var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
18 var ReactCurrentOwner = require('ReactCurrentOwner');
@@ -1038,7 +1039,7 @@ var ReactMount = {
1039 while (container.lastChild) {
1040 container.removeChild(container.lastChild);
1041 }
1041 - container.appendChild(markup);
1042 + DOMLazyTree.insertTreeBefore(container, markup, null);
1043 } else {
1044 setInnerHTML(container, markup);
1045 }
src/renderers/dom/client/utils/DOMChildrenOperations.js
+38 -22
@@ -12,6 +12,7 @@
12
13 'use strict';
14
15 +var DOMLazyTree = require('DOMLazyTree');
16 var Danger = require('Danger');
17 var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
18 var ReactPerf = require('ReactPerf');
@@ -29,21 +30,27 @@ var invariant = require('invariant');
30 * @internal
31 */
32 function insertChildAt(parentNode, childNode, index) {
32 - // By exploiting arrays returning `undefined` for an undefined index, we can
33 - // rely exclusively on `insertBefore(node, null)` instead of also using
34 - // `appendChild(node)`. However, using `undefined` is not allowed by all
35 - // browsers so we must replace it with `null`.
36 -
37 - // fix render order error in safari
38 - // IE8 will throw error when index out of list size.
39 - var beforeChild = index >= parentNode.childNodes.length ?
40 - null :
41 - parentNode.childNodes.item(index);
42 -
43 - parentNode.insertBefore(
44 - childNode,
45 - beforeChild
46 - );
33 + // We can rely exclusively on `insertBefore(node, null)` instead of also using
34 + // `appendChild(node)`. (Using `undefined` is not allowed by all browsers so
35 + // we are careful to use `null`.)
36 +
37 + // In Safari, .childNodes[index] can return a DOM node with id={index} so we
38 + // use .item() instead which is immune to this bug. (See #3560.) In contrast
39 + // to the spec, IE8 throws an error if index is larger than the list size.
40 + var referenceNode =
41 + index < parentNode.childNodes.length ?
42 + parentNode.childNodes.item(index) : null;
43 +
44 + parentNode.insertBefore(childNode, referenceNode);
45 +}
46 +
47 +function insertLazyTreeChildAt(parentNode, childTree, index) {
48 + // See above.
49 + var referenceNode =
50 + index < parentNode.childNodes.length ?
51 + parentNode.childNodes.item(index) : null;
52 +
53 + DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode);
54 }
55
56 /**
@@ -99,9 +106,10 @@ var DOMChildrenOperations = {
106 }
107 }
108
102 - var renderedMarkup;
109 // markupList is either a list of markup or just a list of elements
104 - if (markupList.length && typeof markupList[0] === 'string') {
110 + var isHTML = markupList.length && typeof markupList[0] === 'string';
111 + var renderedMarkup;
112 + if (isHTML) {
113 renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
114 } else {
115 renderedMarkup = markupList;
@@ -118,11 +126,19 @@ var DOMChildrenOperations = {
126 update = updates[k];
127 switch (update.type) {
128 case ReactMultiChildUpdateTypes.INSERT_MARKUP:
121 - insertChildAt(
122 - update.parentNode,
123 - renderedMarkup[update.markupIndex],
124 - update.toIndex
125 - );
129 + if (isHTML) {
130 + insertChildAt(
131 + update.parentNode,
132 + renderedMarkup[update.markupIndex],
133 + update.toIndex
134 + );
135 + } else {
136 + insertLazyTreeChildAt(
137 + update.parentNode,
138 + renderedMarkup[update.markupIndex],
139 + update.toIndex
140 + );
141 + }
142 break;
143 case ReactMultiChildUpdateTypes.MOVE_EXISTING:
144 insertChildAt(
src/renderers/dom/client/utils/DOMLazyTree.js new
+88
@@ -0,0 +1,88 @@
1 +/**
2 + * Copyright 2015, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule DOMLazyTree
10 + */
11 +
12 +'use strict';
13 +
14 +/**
15 + * In IE (8-11) and Edge, appending nodes with no children is dramatically
16 + * faster than appending a full subtree, so we essentially queue up the
17 + * .appendChild calls here and apply them so each node is added to its parent
18 + * before any children are added.
19 + *
20 + * In other browsers, doing so is slower or neutral compared to the other order
21 + * (in Firefox, twice as slow) so we only do this inversion in IE.
22 + *
23 + * See https://github.com/spicyj/innerhtml-vs-createelement-vs-clonenode.
24 + */
25 +var enableLazy = (
26 + typeof document !== 'undefined' &&
27 + typeof document.documentMode === 'number'
28 + ||
29 + typeof navigator !== 'undefined' &&
30 + typeof navigator.userAgent === 'string' &&
31 + /\bEdge\/\d/.test(navigator.userAgent)
32 +);
33 +
34 +function insertTreeChildren(tree) {
35 + if (!enableLazy) {
36 + return;
37 + }
38 + var node = tree.node;
39 + var children = tree.children;
40 + if (children.length) {
41 + for (var i = 0; i < children.length; i++) {
42 + insertTreeBefore(node, children[i], null);
43 + }
44 + } else if (tree.html != null) {
45 + node.innerHTML = tree.html;
46 + }
47 +}
48 +
49 +function insertTreeBefore(parentNode, tree, referenceNode) {
50 + parentNode.insertBefore(tree.node, referenceNode);
51 + insertTreeChildren(tree);
52 +}
53 +
54 +function replaceChildWithTree(oldNode, newTree) {
55 + oldNode.parentNode.replaceChild(newTree.node, oldNode);
56 + insertTreeChildren(newTree);
57 +}
58 +
59 +function queueChild(parentTree, childTree) {
60 + if (enableLazy) {
61 + parentTree.children.push(childTree);
62 + } else {
63 + parentTree.node.appendChild(childTree.node);
64 + }
65 +}
66 +
67 +function queueHTML(tree, html) {
68 + if (enableLazy) {
69 + tree.html = html;
70 + } else {
71 + tree.node.innerHTML = html;
72 + }
73 +}
74 +
75 +function DOMLazyTree(node) {
76 + return {
77 + node: node,
78 + children: [],
79 + html: null,
80 + };
81 +}
82 +
83 +DOMLazyTree.insertTreeBefore = insertTreeBefore;
84 +DOMLazyTree.replaceChildWithTree = replaceChildWithTree;
85 +DOMLazyTree.queueChild = queueChild;
86 +DOMLazyTree.queueHTML = queueHTML;
87 +
88 +module.exports = DOMLazyTree;
src/renderers/dom/shared/Danger.js
+4 -4
@@ -12,6 +12,7 @@
12
13 'use strict';
14
15 +var DOMLazyTree = require('DOMLazyTree');
16 var ExecutionEnvironment = require('ExecutionEnvironment');
17
18 var createNodesFromMarkup = require('createNodesFromMarkup');
@@ -172,13 +173,12 @@ var Danger = {
173 'server rendering. See ReactDOMServer.renderToString().'
174 );
175
175 - var newChild;
176 if (typeof markup === 'string') {
177 - newChild = createNodesFromMarkup(markup, emptyFunction)[0];
177 + var newChild = createNodesFromMarkup(markup, emptyFunction)[0];
178 + oldChild.parentNode.replaceChild(newChild, oldChild);
179 } else {
179 - newChild = markup;
180 + DOMLazyTree.replaceChildWithTree(oldChild, markup);
181 }
181 - oldChild.parentNode.replaceChild(newChild, oldChild);
182 },
183
184 };
src/renderers/dom/shared/ReactDOMComponent.js
+8 -7
@@ -16,6 +16,7 @@
16
17 var AutoFocusUtils = require('AutoFocusUtils');
18 var CSSPropertyOperations = require('CSSPropertyOperations');
19 +var DOMLazyTree = require('DOMLazyTree');
20 var DOMNamespaces = require('DOMNamespaces');
21 var DOMProperty = require('DOMProperty');
22 var DOMPropertyOperations = require('DOMPropertyOperations');
@@ -41,7 +42,6 @@ var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
42 var invariant = require('invariant');
43 var isEventSupported = require('isEventSupported');
44 var keyOf = require('keyOf');
44 -var setInnerHTML = require('setInnerHTML');
45 var setTextContent = require('setTextContent');
46 var shallowEqual = require('shallowEqual');
47 var validateDOMNesting = require('validateDOMNesting');
@@ -667,8 +667,9 @@ ReactDOMComponent.Mixin = {
667 // Populate node cache
668 ReactMount.getID(el);
669 this._updateDOMProperties({}, props, transaction);
670 - this._createInitialChildren(transaction, props, context, el);
671 - mountImage = el;
670 + var lazyTree = DOMLazyTree(el);
671 + this._createInitialChildren(transaction, props, context, lazyTree);
672 + mountImage = lazyTree;
673 } else {
674 var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
675 var tagContent = this._createContentMarkup(transaction, props, context);
@@ -814,12 +815,12 @@ ReactDOMComponent.Mixin = {
815 }
816 },
817
817 - _createInitialChildren: function(transaction, props, context, el) {
818 + _createInitialChildren: function(transaction, props, context, lazyTree) {
819 // Intentional use of != to avoid catching zero/false.
820 var innerHTML = props.dangerouslySetInnerHTML;
821 if (innerHTML != null) {
822 if (innerHTML.__html != null) {
822 - setInnerHTML(el, innerHTML.__html);
823 + DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
824 }
825 } else {
826 var contentToUse =
@@ -827,7 +828,7 @@ ReactDOMComponent.Mixin = {
828 var childrenToUse = contentToUse != null ? null : props.children;
829 if (contentToUse != null) {
830 // TODO: Validate that text is allowed as a child of this node
830 - setTextContent(el, contentToUse);
831 + setTextContent(lazyTree.node, contentToUse);
832 } else if (childrenToUse != null) {
833 var mountImages = this.mountChildren(
834 childrenToUse,
@@ -835,7 +836,7 @@ ReactDOMComponent.Mixin = {
836 context
837 );
838 for (var i = 0; i < mountImages.length; i++) {
838 - el.appendChild(mountImages[i]);
839 + DOMLazyTree.queueChild(lazyTree, mountImages[i]);
840 }
841 }
842 }
src/renderers/dom/shared/ReactDOMTextComponent.js
+2 -1
@@ -13,6 +13,7 @@
13 'use strict';
14
15 var DOMChildrenOperations = require('DOMChildrenOperations');
16 +var DOMLazyTree = require('DOMLazyTree');
17 var DOMPropertyOperations = require('DOMPropertyOperations');
18 var ReactComponentBrowserEnvironment =
19 require('ReactComponentBrowserEnvironment');
@@ -106,7 +107,7 @@ assign(ReactDOMTextComponent.prototype, {
107 // Populate node cache
108 ReactMount.getID(el);
109 setTextContent(el, this._stringText);
109 - return el;
110 + return DOMLazyTree(el);
111 } else {
112 var escapedText = escapeTextContentForBrowser(this._stringText);
113