研究背景
因为最近研究React,对js的Element性能没有直观的印象,找了一些文章,超过3年以上的,内容已经陈旧不堪,很多已经与现代浏览器完全不同。
找到个有代码自测的文章,用Google Chrome
版本 71.0.3578.98(正式版本) (64 位) 重新测试和修正以及增添了一些代码,以便对2020年较新的浏览器下渲染Element性能有个直观的认识。
原文章传送门:各种动态渲染Element方式 的性能探究
代码修正/增加部分:
所有代码均以Element形式返回,并保证返回结果相同。(原文中不少地方返回的内容完全不一致,特别是全createElement的时候很多都少createTextNode,无形中少了不少操作)。
因为是研究React的副产品,新增了用React创建虚拟dom,再渲染至<template/>
获取dom。
测试环境
2020年,win10 1609 , 垃圾笔记本 , 6G内存
chrome 71 , React 16.4
结果和结论
结论:
- 在这些代码之前,写了一些小代码,测试了一下string的拼接效率,因为映像中javascript的字符串拼接效率有问题。测试后发现:
chrome 71 字符串内容本身不大的情况下,使用 +=
拼接效率没什么问题,次数少的时候,比array.join("")用时还少点。所以,基本没有什么字符串拼接效率问题。 - 利用innerHTML转换拼接好的domString整体性能比全dom操作稍高,20%到30%左右的性能差距,次数越多,反倒性能差距在缩小。
- 利用
<template/>
标签作为外围标签,性能较好。 - 传统渲染Element,利用
template
标签的innerHTML - React的虚拟dom,
在次数很多的情况下,性能真的很不错。20200311修正,测试之所以快,是因为template这个DOM没被清空,以至render一直没重建DOM。 - 20200311修正,用字符串拼接为domString,然后一次性用
template
标签的innerHTML转换为domElement效率最高,如果全部用createElement进行dom操作,性能差40%。由于都不是数量级级别的差距,问题并不大。
20200311后续新增更少影响因素的性能测试代码和结果
Start 100
基准:直接cE根div,无字符串拼接,用根div的innerHTML传入字符串,转换为HTMLElement:: 84.000244140625ms
直接cE根div,模板字符串拼接子节点的domString,使用根div的innerText传入,转换为 HTMLElement:: 87ms
cE临时div,模板字符串拼接全部domString,使用临时div的innerText转换为 HTMLElement:: 85.999755859375ms
cE创建临时template,模板字符串拼接全部domString,使用临时template的innerText转换为 HTMLElement: 62.000244140625ms
cDF创建临时df,ce创建根div,模板字符串拼接子节点domString,使用根div的innerText转换为 HTMLElement: 79ms
React的CreateElement创建虚拟dom,再用template恢复HTMLElement: 128.999755859375ms
基准:直接createElement外层div,直接createElement内层每个child,用appendChild添加: 133.000244140625ms
createDocumentFragment创建外层,主与子均用createElement创建,用appendChild添加: 103ms
cE创建临时template ,cE创建所有element: 105ms
Start 1000
基准:直接cE根div,无字符串拼接,用根div的innerHTML传入字符串,转换为HTMLElement:: 793ms
直接cE根div,模板字符串拼接子节点的domString,使用根div的innerText传入,转换为 HTMLElement:: 803ms
cE临时div,模板字符串拼接全部domString,使用临时div的innerText转换为 HTMLElement:: 727ms
cE创建临时template,模板字符串拼接全部domString,使用临时template的innerText转换为 HTMLElement: 583.999755859375ms
cDF创建临时df,ce创建根div,模板字符串拼接子节点domString,使用根div的innerText转换为 HTMLElement: 764.000244140625ms
React的CreateElement创建虚拟dom,再用template恢复HTMLElement: 184ms
基准:直接createElement外层div,直接createElement内层每个child,用appendChild添加: 905ms
createDocumentFragment创建外层,主与子均用createElement创建,用appendChild添加: 1145ms
cE创建临时template ,cE创建所有element: 913ms
测试代码
测试代码还是放最后面,如果有兴趣添加更多的方法或者进行最新的验证,可参考以下代码:
tt.html
<!DOCTYPE html>
<html lang="zh_cn">
<head>
<meta charset="UTF-8">
<title>tt</title>
<!-- <script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script> -->
<!-- <script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script> -->
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.production.min.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.production.min.js"></script>
<script type="text/javascript" src="js\React_domString.js"></script>
<script type="text/javascript" src="js\tt2.js"></script>
</head>
<body>
<div id="base">this is a test</div>
<div id="tdom" style="display:none"></div>
<template id="tplt"></template>
</body>
</html>
tt2.js(置于tt.html所在目录的子目录js之下)
/**
* @param Count:渲染DOM结构的次数
*/
var DateCount = {
TimeList : {},
time:function(Str){
console.time(Str);
},
timeEnd:function(Str){
console.timeEnd(Str);
}
};
function compRslt(bRslt,nRslt){
if(! (bRslt.outerHTML === nRslt.outerHTML)) console.log(["rslt not eq","\nb=",bRslt.outerHTML,"\nn=",nRslt.outerHTML].join(""));
}
var Test = function(Count){
let baseRslt,nowRslt,testTitle;
//基准测试1:
nowRslt = null;
testTitle = "基准:直接cE根div,无字符串拼接,用根div的innerHTML传入字符串,转换为HTMLElement:";
DateCount.time(testTitle);
for (let index = 0; index < Count; index++) {
nowRslt=(function(){
let 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>' //需要增加的一大段Element,共100个子级div
return template
}())
}
baseRslt = nowRslt;
let rsltDiv = document.querySelector("#base");
if (!rsltDiv.childElementCount) rsltDiv.appendChild(baseRslt);
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
//直接创建根div,用根div的innerHTML传入文档字符串
nowRslt = null;
testTitle = "直接cE根div,模板字符串拼接子节点的domString,使用根div的innerText传入,转换为 HTMLElement:";
// DateCount.time("临时div + 字符串拼接:")
DateCount.time(testTitle);
for (let index = 0; index < Count; index++) {
nowRslt=(function(){
let template = document.createElement("div");
template.className = "TestClass";
template.setAttribute("Arg","TestArg")
template.innerHTML = `Test TextNode${(function(){
let temp = "";
for (let index = 0; index < 100; index++) {
temp+="<div child='true'>M</div>"
}
return temp
}())}` //需要增加的一大段Element
return template;
}())
}
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
//临时div + 临时字符串拼接:
nowRslt = null;
testTitle = "cE临时div,模板字符串拼接全部domString,使用临时div的innerText转换为 HTMLElement:";
// DateCount.time("临时div + 字符串拼接:")
DateCount.time(testTitle);
for (let index = 0; index < Count; index++) {
nowRslt=(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;
}())
}
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
//临时template + 临时字符串拼接:
nowRslt = null;
testTitle = "cE创建临时template,模板字符串拼接全部domString,使用临时template的innerText转换为 HTMLElement";
// DateCount.time("临时template + 字符串拼接:")
DateCount.time(testTitle);
for (let index = 0; index < Count; index++) {
nowRslt=(function(){
let 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.firstChild;
}())
}
// console.log(nowRslt);
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
//createDocumentFragment + 临时字符串拼接:
// DocumentFragment 没有 innerText属性
nowRslt = null;
testTitle = "cDF创建临时df,ce创建根div,模板字符串拼接子节点domString,使用根div的innerText转换为 HTMLElement";
// DateCount.time("临时template + 字符串拼接:")
DateCount.time(testTitle);
for (let index = 0; index < Count; index++) {
nowRslt=(function(){
let fragment = document.createDocumentFragment();
fragment.appendChild(function(){
let template = document.createElement("div");
template.className = "TestClass";
template.setAttribute("Arg","TestArg")
template.innerHTML = `Test TextNode${(function(){
let temp = "";
for (let index = 0; index < 100; index++) {
temp+="<div child='true'>M</div>"
}
return temp
}())}` //需要增加的一大段Element
return template;
}());
return fragment.firstChild
}())
}
// console.log(nowRslt);
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
//react的虚拟Dom
nowRslt = null;
testTitle = "React的CreateElement创建虚拟dom,再用template恢复HTMLElement";
DateCount.time(testTitle);
for (let idx = 0; idx < Count; idx++){
nowRslt = (function(){
let cDomArr=["Test TextNode"];
for (let index = 0 ; index < 100; index++){
cDomArr.push(React.createElement("div",{child:"true"},"M"));
};
let vDom = React.createElement("div",{className:"TestClass",Arg:"TestArg"},cDomArr);
ReactDOM.render(vDom,document.getElementById("tplt"));
let rDom = document.getElementById("tplt");
return rDom.firstChild;
}())
}
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
//基准测试2:
nowRslt = null;
testTitle = "基准:直接createElement外层div,直接createElement内层每个child,用appendChild添加";
// DateCount.time("createElement+appendChild写法:")
DateCount.time(testTitle);
for (let index = 0; index < Count; index++) {
nowRslt=(function(){
let 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
}())
}
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
//DocumentFragment
nowRslt = null;
testTitle = "createDocumentFragment创建外层,主与子均用createElement创建,用appendChild添加";
// DateCount.time("DocumentFragment+ createElement+appendChild 写法:")
DateCount.time(testTitle);
for (let index = 0; index < Count; index++) {
nowRslt=(function(){
let fragment = document.createDocumentFragment();
fragment.appendChild(function(){
let 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;
}());
return fragment.firstChild
}())
}
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
//临时template + createElement+appendChild 写法
nowRslt = null;
testTitle = "cE创建临时template ,cE创建所有element";
DateCount.time(testTitle);
// DateCount.time("template + createElement+appendChild 写法:")
for (let index = 0; index < Count; index++) {
nowRslt=(function(){
let template = document.createElement("template");
template.appendChild(function(){
let 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;
}());
return template.firstChild
}())
}
compRslt(baseRslt,nowRslt);
DateCount.timeEnd(testTitle);
};
window.onload = function(){
// for (let key of [1,10,100,1000]) {
// 100个div,1000次,就是10w个dom
for (let key of [100,1000]) {
console.log("Start "+key);
Test(key);
}
}
新增测试代码
var Test2 = function(Count){
let baseRslt,nowRslt,testTitle,template;
// createElement并append的性能
nowRslt , template= null,null;
testTitle = "a在template中cE元素,append到template中,共计Count 次 :";
DateCount.time(testTitle);
template = document.createElement("template");
let fragment = template.content
for(let idx = 0; idx < Count; idx++){
let cEl = document.createElement("div");
cEl.className = "tclass";
cEl.id = "test";
cEl.setAttribute("style","color: blue;");
cEl.appendChild(document.createTextNode("this is a test div"));
fragment.appendChild(cEl);
};
nowRslt = template
baseRslt = template
compRslt(baseRslt.outerHTML,nowRslt.outerHTML);
DateCount.timeEnd(testTitle);
// 生成domString,通过innerText附加到临时template
nowRslt , template= null,null;
testTitle = "b生成domString,通过innerText附加到临时template,共计Count 次:";
DateCount.time(testTitle);
template = document.createElement("template");
let tSt = "";
for(let idx = 0; idx < Count; idx++){
let cEl = `<div class="tclass" id="test" style="color: blue;">this is a test div</div>`;
tSt += cEl;
};
template.innerHTML = tSt;
nowRslt = template
compRslt(baseRslt.outerHTML,nowRslt.outerHTML);
DateCount.timeEnd(testTitle);
// 生成vDom,通过ReactDOM.render渲染
nowRslt , template= null,null;
testTitle = "c生成vDom,通过ReactDOM.render渲染,共计Count 次:";
DateCount.time(testTitle);
template = document.createElement("template");
let vDom;
let vChild = []
for(let idx = 0; idx < Count; idx++){
vChild.push(React.createElement("div",{className:"tclass",id:"test",style:{color:"blue"}},"this is a test div"))
};
vDom = React.createElement("span",{},...vChild);
ReactDOM.render(vDom,template);
nowRslt = template.firstChild
compRslt(baseRslt.innerHTML,nowRslt.innerHTML);
DateCount.timeEnd(testTitle);
};
window.onload = function(){
// for (let key of [1,10,100,1000]) {
// 100个div,1000次,就是10w个dom
for (let key of [1000,10000]) {
console.log("\n-------------\nStart ",key);
Test2(key);
console.log("End")
}
}
新增代码测试结果如下
-------------
Start 10000
a在template中cE元素,append到template中,共计Count 次 :: 265ms
b生成domString,通过innerText附加到临时template,共计Count 次:: 166ms
c生成vDom,通过ReactDOM.render渲染,共计Count 次:: 865ms
End
-------------
Start 50000
a在template中cE元素,append到template中,共计Count 次 :: 1164ms
b生成domString,通过innerText附加到临时template,共计Count 次:: 854ms
c生成vDom,通过ReactDOM.render渲染,共计Count 次:: 3038ms
End