研究背景
因爲最近研究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