###一、性能优化的原则及方法论
树立原则:动态渲染进入一个Dom元素,首先需要保证动态渲染操作必须尽可能少对原有dom树的影响,影响重绘及重排。
确定方法论:必须寻找一个容器来缓存渲染期间生成的dom结构(操作必须尽可能少对原有dom树的影响),然后再进行一次渲染到目标element中。
二、生成期间DOM缓存的选择
- DocumentFragment(文档碎片对象,选择原因:脱离于文档流)
- 临时Element(选择原因:新element脱离于文档流)
- createElement,再一步步进行渲染
- 通过描述Dom的String(下称:DomString),转化为Dom对象
- 临时Element+innerHTML+cloneNode返回最外层element元素对象,再进行插入appendChild,必要时还需要选择器方法讲某一个Element对象提取出来
- XML字符串通过解析生成Element对象(注意,不是HTMLxxxElement对象,是Element对象),然后将该对象appendChild进去
- 临时字符串(选择原因:借助innerHTML渲染,一次渲染)
###三、DocumentFragment的优缺点
基本模式:
var fragment = document.createDocumentFragment();
fragment.appendChild(
... //生成Element的IIFE
)
//IIFE示例,根据配置创建元素
var ConfigVar = {
ELname:"div",
id:"blablabla",
name:"balblabla",
class:"ClassName"
}
(function(Config){
var el = document.createElement(Config.ELname);
el.className = (Config.class || "");
for (let AttrName in Config){
if (AttrName == "class")continue;
el.setAttribute(AttrName,Config[AttrName]);
}
return el;
})(ConfigVar)
优点
1、脱离于文档流,操作不会对Dom树产生影响
2、在每一次生成临时Element时候就可以将该Element对象的引用保存下来,而不需要多次用选择器再次获取。
缺点
兼容性只是达到IE9+
http://caniuse.com/#search=DocumentFragment
四、createElement的优缺点
基本模式
var el = document.createElement("ElementName");
el.className = "";
el.setAttribute("AttrName",AttrValue);
el.setAttribute("AttrName",AttrValue);
...
el.appendChild(
... //生成Element的IIFE,见上文
);
优点
1、新创建的元素脱离于文档流,操作不会对Dom树产生影响
2、兼容性最好
3、在每一次生成临时Element时候就可以将该Element对象的引用保存下来,而不需要多次用选择器再次获取。
缺点
每一次调用setAttribute
方法都是一次次对Element进行修改,此处具有潜在的性能损耗。
五、DomString——临时Element+innerHTML+cloneNode的优缺点
基本模式
var domString2Dom = (function(){
if (window.HTMLTemplateElement){
var container = document.createElement("template");
return function(domString){
container.innerHTML = domString;
return container.content.firstChild.cloneNode(true)
}
}else{
//对不支持的template 的浏览器还有兼容性方法没写,所以不支持tr,td等些元素inner进div中。
var container = document.createElement("div");
return function(domString){
container.innerHTML = domString;
return container.firstChild.cloneNode(true)
}
}
})();
var template = domString2Dom('<div class="TestClass" Arg="TestArg"></div>');
for (var index = 0; index < 80; index++) {
template.appendChild(
(function(){
var el = domString2Dom("<div>M</div>");
return el
})()
)
}
优点
创建Dom之后不需要多次进行setAttribute
缺点
1、临时元素不能包裹一些特定的元素(不能在所有浏览器的所有 HTML 元素上设置 innerHTML 属性)
2、解析的过程进行了很多其余的操作。此处具有潜在的性能损耗。
3、插入的字符串第一层Node只允许有一个元素
六、DomString——XML解析的优缺点
基本模式
var XMLParser = function () {
var $DOMParser = new DOMParser();
return function (domString) {
if (domString[0] == "<") {
var doc = $DOMParser.parseFromString(domString, "application/xhtml+xml");
return doc.firstChild;
}
else {
return document.createTextNode(domString);
}
};
}();
var template = XMLParser('<div class="TestClass" Arg="TestArg"></div>');
for (var index = 0; index < 80; index++) {
template.appendChild((function () {
var el = XMLParser("<div>M</div>");
return el;
})());
}
优点
DomString方法中通用性最强的,虽然IE10+才支持DOMParser,但是IE9以下的有替代方法
缺点
1、解析的过程本身就具有潜在的性能损耗。
2、只能得到刚刚创建最外层元素的克隆。子元素的引用还需要用选择器。
3、插入的字符串第一层Node只允许有一个元素
七、临时字符串的优缺点
基本模式:
var template = document.createElement("div");
template.innerHTML = `<div class="TestClass" Arg="TestArg">
Test TextNode
${(function(){
var temp = new Array(8);
for (var index = 0; index < 80; index++) {
temp[index]="<div>M</div>"
}
return temp.join()
}())}
</div>` //需要增加的一大段Element
优点
1、通用性最强,不需要逐步创建一大堆无用的Element对象引用
2、运用es6模板字符串编码优雅,不需要字符串用加号进行链接
缺点
1、如果是直接给出配置Config进行渲染需要进行字符串的生成
2、只能得到刚刚创建最外层元素的引用。子元素的引用还需要用选择器。
八、Template元素
由于HTML5中新增了template元素
其特点就是有一个content属性是HTMLDocumentFragment对象,所以可以包容任何元素
基本范式是:
var template = document.createElement("template");
template.innerHTML = `<div class="TestClass" Arg="TestArg">
Test TextNode
${(function(){
var temp = new Array(8);
for (var index = 0; index < 80; index++) {
temp[index]="<div>M</div>"
}
return temp.join()
}())}
</div>` //需要增加的一大段Element
// template.content 是HTMLDocumentFragment
优点
比div要好很多,作为临时元素容器的包容性更强
缺点
兼容性不好:http://caniuse.com/#search=HTML%20templates 在不支持的浏览器中表示为HTMLUnknownElement
九、各种方法的效率对比
测试代码:(由于笔者不太熟悉各种浏览器性能的BUG,这里的代码如果有不足请指正),代码由typescript进行编写,也可以用babel进行编译。
/**
* @param Count:渲染DOM结构的次数
*/
var DateCount = {
TimeList : {},
time:function(Str){
console.time(Str);
},
timeEnd:function(Str){
console.timeEnd(Str);
}
};
//==================工具函数======================
var domString2Dom = (function () {
var container;
if (window.HTMLTemplateElement) {
container = document.createElement("template");
return function (domString) {
container.innerHTML = domString;
return container.content.firstChild.cloneNode(true);
};
}
else {
//对不支持的template 的浏览器还有兼容性方法没写,所以不支持tr,td等些元素inner进div中。
container = document.createElement("div");
return function (domString) {
container.innerHTML = domString;
return container.firstChild.cloneNode(true);
};
}
})();
var XMLParser = (function () {
var $DOMParser;
if (window.DOMParser) {
$DOMParser = new DOMParser();
return function (domString) {
if (domString[0] == "<") {
var doc = $DOMParser.parseFromString(domString, "application/xhtml+xml");
return doc.firstChild;
}
else {
return document.createTextNode(domString);
}
};
}else{
$DOMParser = new ActiveXObject("Microsoft.XMLDOM");
return function (domString) {
if (domString[0] == "<") {
$DOMParser.async = false;
$DOMParser.loadXML(domString);
return $DOMParser
}
else {
return document.createTextNode(domString);
}
}
}
})();
//===============================================
var Test = function(Count){
//保留这种写法,能够在移动端平台中不依靠控制台进行效率测试
// var DateCount = {
// TimeList : {},
// time:function(Str){
// this.TimeList[Str] = Date.now();
// },
// timeEnd:function(Str){
// alert(Str+(Date.now() - this.TimeList[Str]));
// }
// }
//基准测试1:
DateCount.time("无临时div + 不需要字符串拼接 + innerHTML:")
for (let index = 0; index < Count; index++) {
(function(){
var template = document.createElement("div");
template.className = "TestClass";
template.setAttribute("Arg","TestArg")
template.innerHTML = ` Test TextNode
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
<div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div><div child="true">M</div>
` //需要增加的一大段Element,共100个子级div
return template
}())
}
DateCount.timeEnd("无临时div + 不需要字符串拼接 + innerHTML:")
//基准测试2:
DateCount.time("createElement+appendChild写法:")
for (let index = 0; index < Count; index++) {
(function(){
var template = document.createElement("div");
template.className = "TestClass";
template.setAttribute("Arg","TestArg")
template.appendChild(document.createTextNode('Test TextNode'));
for (let index = 0; index < 100; index++) {
let element = document.createElement("div");
element.setAttribute("child","true");
element.appendChild(document.createTextNode("M"))
template.appendChild(element)
}
return template
}())
}
DateCount.timeEnd("createElement+appendChild写法:")
//DocumentFragment
DateCount.time("DocumentFragment+ createElement+appendChild 写法:")
for (let index = 0; index < Count; index++) {
(function(){
var fragment = document.createDocumentFragment();
fragment.appendChild(function(){
var template = document.createElement("div");
template.className = "TestClass";
template.setAttribute("Arg","TestArg")
template.appendChild(document.createTextNode('Test TextNode'));
for (let index = 0; index < 100; index++) {
let element = document.createElement("div");
element.setAttribute("child","true");
template.appendChild(element)
}
return template;
}());
return fragment
}())
}
DateCount.timeEnd("DocumentFragment+ createElement+appendChild 写法:")
//DomString——临时Element+innerHTML+cloneNode
// DateCount.time("DomString——临时Element+innerHTML+cloneNode:")
// for (let index = 0; index < Count; index++) {
// (function(){
// var template = domString2Dom('<div class="TestClass" Arg="TestArg"></div>');
// for (let index = 0; index < 100; index++) {
// template.appendChild(
// (function(){
// var el = domString2Dom("<div child='true'>M</div>");
// return el
// })()
// )
// }
// return template;
// }())
// }
// DateCount.timeEnd("DomString——临时Element+innerHTML+cloneNode:")
//DomString——XML解析
// DateCount.time("DomString——XML解析:")
// for (let index = 0; index < Count; index++) {
// (function(){
// var template = XMLParser('<div class="TestClass" Arg="TestArg"></div>');
// for (let index = 0; index < 100; index++) {
// template.appendChild((function () {
// var el = XMLParser("<div child='true'>M</div>");
// return el;
// })());
// }
// }())
// }
// DateCount.timeEnd("DomString——XML解析:")
//临时div + 临时字符串拼接:
DateCount.time("临时div + 字符串拼接:")
for (let index = 0; index < Count; index++) {
(function(){
let template = document.createElement("div");
template.innerHTML = `<div class="TestClass" Arg="TestArg">
Test TextNode
${(function(){
let temp = "";
for (let index = 0; index < 100; index++) {
temp+="<div child='true'>M</div>"
}
return temp
}())}
</div>` //需要增加的一大段Element
return template.firstChild;
}())
}
DateCount.timeEnd("临时div + 字符串拼接:")
//临时template + 临时字符串拼接:
DateCount.time("临时template + 字符串拼接:")
for (let index = 0; index < Count; index++) {
(function(){
var template = document.createElement("template");
template.innerHTML = `<div class="TestClass" Arg="TestArg">
Test TextNode
${(function(){
let temp = "";
for (let index = 0; index < 100; index++) {
temp+="<div child='true'>M</div>"
}
return temp
}())}
</div>` //需要增加的一大段Element
return template.content;
}())
}
DateCount.timeEnd("临时template + 字符串拼接:")
//临时template + createElement+appendChild 写法
DateCount.time("template + createElement+appendChild 写法:")
for (let index = 0; index < Count; index++) {
(function(){
var template = document.createElement("template");
template.appendChild(function(){
var template = document.createElement("div");
template.className = "TestClass";
template.setAttribute("Arg","TestArg")
template.appendChild(document.createTextNode('Test TextNode'));
for (let index = 0; index < 100; index++) {
let element = document.createElement("div");
element.setAttribute("child","true");
template.appendChild(element)
}
return template;
}());
return template.content
}())
}
DateCount.timeEnd("template + createElement+appendChild 写法:")
};
for (var key of [1,10,100,1000]) {
console.log("Start"+key);
Test(key);
}
十、结论
经过笔者基本依据手上平台进行测试,
- 无临时div + 不需要字符串拼接 + innerHTML // createElement+appendChild写法:性能低,无论在桌面端还是移动端,在IE/Edge系还是 Webkit系都是同样的表现
- domString 方法:性能最差
- DocumentFragment+ createElement+appendChild 写法:性能在桌面WebKit端表现最好,移动端也有不错的表现
- 字符串拼接:临时div + 字符串拼接/临时template + 字符串拼接:性能表现基本一致,桌面端WebKit上:比DocumentFragment(或tmplate) + createElement+appendChild性能差多了;与之相反,在IE系inneHTML的性能最高,是前者的x十倍。在移动端上两者性能相近,innerHTML略差一点点。
- template + createElement+appendChild 写法:与DocumentFragment+ createElement+appendChild 写法效率相仿。
具体数据测试之后再补充。
(待续)