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