【引自雕刻零碎的博客】一、性能優化的原則及方法論
樹立原則:動態渲染進入一個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 寫法效率相仿。
具體數據測試之後再補充。
(待續)
點讚 0