效率测试:动态渲染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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值