效率測試:動態渲染Element方式性能跟進研究-2020年chrome

研究背景

因爲最近研究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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章