diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index bdde2483c4..3948e9958c 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -12,6 +12,7 @@ 'use strict'; var ClientReactRootIndex = require('ClientReactRootIndex'); +var DOMLazyTree = require('DOMLazyTree'); var DOMProperty = require('DOMProperty'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactCurrentOwner = require('ReactCurrentOwner'); @@ -1038,7 +1039,7 @@ var ReactMount = { while (container.lastChild) { container.removeChild(container.lastChild); } - container.appendChild(markup); + DOMLazyTree.insertTreeBefore(container, markup, null); } else { setInnerHTML(container, markup); } diff --git a/src/renderers/dom/client/utils/DOMChildrenOperations.js b/src/renderers/dom/client/utils/DOMChildrenOperations.js index 3fd79c5414..255e634688 100644 --- a/src/renderers/dom/client/utils/DOMChildrenOperations.js +++ b/src/renderers/dom/client/utils/DOMChildrenOperations.js @@ -12,6 +12,7 @@ 'use strict'; +var DOMLazyTree = require('DOMLazyTree'); var Danger = require('Danger'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); var ReactPerf = require('ReactPerf'); @@ -29,21 +30,27 @@ var invariant = require('invariant'); * @internal */ function insertChildAt(parentNode, childNode, index) { - // By exploiting arrays returning `undefined` for an undefined index, we can - // rely exclusively on `insertBefore(node, null)` instead of also using - // `appendChild(node)`. However, using `undefined` is not allowed by all - // browsers so we must replace it with `null`. + // We can rely exclusively on `insertBefore(node, null)` instead of also using + // `appendChild(node)`. (Using `undefined` is not allowed by all browsers so + // we are careful to use `null`.) - // fix render order error in safari - // IE8 will throw error when index out of list size. - var beforeChild = index >= parentNode.childNodes.length ? - null : - parentNode.childNodes.item(index); + // In Safari, .childNodes[index] can return a DOM node with id={index} so we + // use .item() instead which is immune to this bug. (See #3560.) In contrast + // to the spec, IE8 throws an error if index is larger than the list size. + var referenceNode = + index < parentNode.childNodes.length ? + parentNode.childNodes.item(index) : null; - parentNode.insertBefore( - childNode, - beforeChild - ); + parentNode.insertBefore(childNode, referenceNode); +} + +function insertLazyTreeChildAt(parentNode, childTree, index) { + // See above. + var referenceNode = + index < parentNode.childNodes.length ? + parentNode.childNodes.item(index) : null; + + DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode); } /** @@ -99,9 +106,10 @@ var DOMChildrenOperations = { } } - var renderedMarkup; // markupList is either a list of markup or just a list of elements - if (markupList.length && typeof markupList[0] === 'string') { + var isHTML = markupList.length && typeof markupList[0] === 'string'; + var renderedMarkup; + if (isHTML) { renderedMarkup = Danger.dangerouslyRenderMarkup(markupList); } else { renderedMarkup = markupList; @@ -118,11 +126,19 @@ var DOMChildrenOperations = { update = updates[k]; switch (update.type) { case ReactMultiChildUpdateTypes.INSERT_MARKUP: - insertChildAt( - update.parentNode, - renderedMarkup[update.markupIndex], - update.toIndex - ); + if (isHTML) { + insertChildAt( + update.parentNode, + renderedMarkup[update.markupIndex], + update.toIndex + ); + } else { + insertLazyTreeChildAt( + update.parentNode, + renderedMarkup[update.markupIndex], + update.toIndex + ); + } break; case ReactMultiChildUpdateTypes.MOVE_EXISTING: insertChildAt( diff --git a/src/renderers/dom/client/utils/DOMLazyTree.js b/src/renderers/dom/client/utils/DOMLazyTree.js new file mode 100644 index 0000000000..8fd6a67bec --- /dev/null +++ b/src/renderers/dom/client/utils/DOMLazyTree.js @@ -0,0 +1,88 @@ +/** + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule DOMLazyTree + */ + +'use strict'; + +/** + * In IE (8-11) and Edge, appending nodes with no children is dramatically + * faster than appending a full subtree, so we essentially queue up the + * .appendChild calls here and apply them so each node is added to its parent + * before any children are added. + * + * In other browsers, doing so is slower or neutral compared to the other order + * (in Firefox, twice as slow) so we only do this inversion in IE. + * + * See https://github.com/spicyj/innerhtml-vs-createelement-vs-clonenode. + */ +var enableLazy = ( + typeof document !== 'undefined' && + typeof document.documentMode === 'number' + || + typeof navigator !== 'undefined' && + typeof navigator.userAgent === 'string' && + /\bEdge\/\d/.test(navigator.userAgent) +); + +function insertTreeChildren(tree) { + if (!enableLazy) { + return; + } + var node = tree.node; + var children = tree.children; + if (children.length) { + for (var i = 0; i < children.length; i++) { + insertTreeBefore(node, children[i], null); + } + } else if (tree.html != null) { + node.innerHTML = tree.html; + } +} + +function insertTreeBefore(parentNode, tree, referenceNode) { + parentNode.insertBefore(tree.node, referenceNode); + insertTreeChildren(tree); +} + +function replaceChildWithTree(oldNode, newTree) { + oldNode.parentNode.replaceChild(newTree.node, oldNode); + insertTreeChildren(newTree); +} + +function queueChild(parentTree, childTree) { + if (enableLazy) { + parentTree.children.push(childTree); + } else { + parentTree.node.appendChild(childTree.node); + } +} + +function queueHTML(tree, html) { + if (enableLazy) { + tree.html = html; + } else { + tree.node.innerHTML = html; + } +} + +function DOMLazyTree(node) { + return { + node: node, + children: [], + html: null, + }; +} + +DOMLazyTree.insertTreeBefore = insertTreeBefore; +DOMLazyTree.replaceChildWithTree = replaceChildWithTree; +DOMLazyTree.queueChild = queueChild; +DOMLazyTree.queueHTML = queueHTML; + +module.exports = DOMLazyTree; diff --git a/src/renderers/dom/shared/Danger.js b/src/renderers/dom/shared/Danger.js index 578c109d42..04dbe88a35 100644 --- a/src/renderers/dom/shared/Danger.js +++ b/src/renderers/dom/shared/Danger.js @@ -12,6 +12,7 @@ 'use strict'; +var DOMLazyTree = require('DOMLazyTree'); var ExecutionEnvironment = require('ExecutionEnvironment'); var createNodesFromMarkup = require('createNodesFromMarkup'); @@ -172,13 +173,12 @@ var Danger = { 'server rendering. See ReactDOMServer.renderToString().' ); - var newChild; if (typeof markup === 'string') { - newChild = createNodesFromMarkup(markup, emptyFunction)[0]; + var newChild = createNodesFromMarkup(markup, emptyFunction)[0]; + oldChild.parentNode.replaceChild(newChild, oldChild); } else { - newChild = markup; + DOMLazyTree.replaceChildWithTree(oldChild, markup); } - oldChild.parentNode.replaceChild(newChild, oldChild); }, }; diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js index c5b061dd19..f482bbf43d 100644 --- a/src/renderers/dom/shared/ReactDOMComponent.js +++ b/src/renderers/dom/shared/ReactDOMComponent.js @@ -16,6 +16,7 @@ var AutoFocusUtils = require('AutoFocusUtils'); var CSSPropertyOperations = require('CSSPropertyOperations'); +var DOMLazyTree = require('DOMLazyTree'); var DOMNamespaces = require('DOMNamespaces'); var DOMProperty = require('DOMProperty'); var DOMPropertyOperations = require('DOMPropertyOperations'); @@ -41,7 +42,6 @@ var escapeTextContentForBrowser = require('escapeTextContentForBrowser'); var invariant = require('invariant'); var isEventSupported = require('isEventSupported'); var keyOf = require('keyOf'); -var setInnerHTML = require('setInnerHTML'); var setTextContent = require('setTextContent'); var shallowEqual = require('shallowEqual'); var validateDOMNesting = require('validateDOMNesting'); @@ -667,8 +667,9 @@ ReactDOMComponent.Mixin = { // Populate node cache ReactMount.getID(el); this._updateDOMProperties({}, props, transaction); - this._createInitialChildren(transaction, props, context, el); - mountImage = el; + var lazyTree = DOMLazyTree(el); + this._createInitialChildren(transaction, props, context, lazyTree); + mountImage = lazyTree; } else { var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props); var tagContent = this._createContentMarkup(transaction, props, context); @@ -814,12 +815,12 @@ ReactDOMComponent.Mixin = { } }, - _createInitialChildren: function(transaction, props, context, el) { + _createInitialChildren: function(transaction, props, context, lazyTree) { // Intentional use of != to avoid catching zero/false. var innerHTML = props.dangerouslySetInnerHTML; if (innerHTML != null) { if (innerHTML.__html != null) { - setInnerHTML(el, innerHTML.__html); + DOMLazyTree.queueHTML(lazyTree, innerHTML.__html); } } else { var contentToUse = @@ -827,7 +828,7 @@ ReactDOMComponent.Mixin = { var childrenToUse = contentToUse != null ? null : props.children; if (contentToUse != null) { // TODO: Validate that text is allowed as a child of this node - setTextContent(el, contentToUse); + setTextContent(lazyTree.node, contentToUse); } else if (childrenToUse != null) { var mountImages = this.mountChildren( childrenToUse, @@ -835,7 +836,7 @@ ReactDOMComponent.Mixin = { context ); for (var i = 0; i < mountImages.length; i++) { - el.appendChild(mountImages[i]); + DOMLazyTree.queueChild(lazyTree, mountImages[i]); } } } diff --git a/src/renderers/dom/shared/ReactDOMTextComponent.js b/src/renderers/dom/shared/ReactDOMTextComponent.js index 39ad8a13cd..9f5c42deb3 100644 --- a/src/renderers/dom/shared/ReactDOMTextComponent.js +++ b/src/renderers/dom/shared/ReactDOMTextComponent.js @@ -13,6 +13,7 @@ 'use strict'; var DOMChildrenOperations = require('DOMChildrenOperations'); +var DOMLazyTree = require('DOMLazyTree'); var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); @@ -106,7 +107,7 @@ assign(ReactDOMTextComponent.prototype, { // Populate node cache ReactMount.getID(el); setTextContent(el, this._stringText); - return el; + return DOMLazyTree(el); } else { var escapedText = escapeTextContentForBrowser(this._stringText);