前端工程師必備:從瀏覽器的渲染到性能優化
一、問題前瞻
1. 為什么css需要放在頭部? 2. js為什么要放在body后面? 3. 圖片的加載和渲染會阻塞頁面DOM構建嗎? 4. dom解析完才出現頁面嗎? 5. 首屏時間根據什么來判定?
二、瀏覽器渲染
1、瀏覽器渲染圖解
[來自google開發者文檔]
瀏覽器渲染頁面主要經歷了下面的步驟:
1.處理 HTML 標記并構建 DOM 樹。 2.處理 CSS 標記并構建 CSSOM 樹。 3.將 DOM 與 CSSOM 合并成一個渲染樹。 4.根據渲染樹來布局,以計算每個節點的幾何信息。 5.將各個節點繪制到屏幕上。
為構建渲染樹,瀏覽器大體上完成了下列工作:
從 DOM 樹的根節點開始遍歷每個可見節點。 某些節點不可見(例如腳本標記、元標記等),因為它們不會體現在渲染輸出中,所以會被忽略。 某些節點通過 CSS 隱藏,因此在渲染樹中也會被忽略,例如,上例中的 span 節點---不會出現在渲染樹中,---因為有一個顯式規則在該節點上設置了“display: none”屬性。 對于每個可見節點,為其找到適配的 CSSOM 規則并應用它們。 發射可見節點,連同其內容和計算的樣式。
根據以上解析,DOM樹和CSSOM樹的構建對于頁面性能有非常大的影響,沒有DOM樹,頁面基本的標簽塊都沒有,沒有樣式,頁面也基本是空白的。所以具體css的解析規則是什么?js是怎么影響頁面渲染的?了解了這些,我們才能有的放矢,對頁面性能進行優化。
2.css解析規則
1
從左向右的匹配規則
從右向左的匹配規則
如果css從左向右解析,意味著我們需要遍歷更多的節點。不管樣式規則寫得多細致,每一個dom結點仍然需要遍歷,因為整個style rules還會有其它公共樣式影響。如果從右向左解析,因為子元素只有一個父元素,所以能夠很快定位出當前dom符不符合樣式規則。
3.js加載和執行機制
首先明確一點,我們可以通過js去修改網頁的內容,樣式和交互等,這一意味著js會影響頁面的dom結構,如果js和dom構建并行執行,那么很容易會出現沖突,所以js在執行時必然會阻塞dom和cssom的構建過程,不論是外部js還是內聯腳本。
js的位置是否影響dom解析?
首先我們為什么提倡把js放在body標簽的后面去加載,因為從demo上看無論是放在head還是放在body后加載js,頁面domcontentload的時間都是一樣的:
我們從圖中可以看出js的加載和執行是阻塞dom解析的,但是因為頁面并不是一次就渲染完成,所以我們需要做的是盡量讓用戶看到首屏的部分被渲染出來,js放在頭部,則頁面的內容區域還沒有解析到就被阻塞了,導致用戶看到的是白屏,而js放在body后面,盡管此時頁面dom仍然沒有解析完成,但是已經渲染出一部分樓層了,這也是為什么我們比較看重頁面的首屏時間。
只有DOM和CSSOM樹構建好后并合并成渲染樹才能開始繪制頁面圖形,那是不是把整個DOM樹和CSSOM樹構建好后才能開始繪制頁面?這顯然是不符合我們平時訪問頁面的認知的,實際上:
為達到更好的用戶體驗,呈現引擎會力求盡快將內容顯示在屏幕上。它不必等到整個 HTML 文檔解析完畢之后,就會開始構建呈現樹和設置布局。在不斷接收和處理來自網絡的其余內容的同時,呈現引擎會將部分內容解析并顯示出來。
具體瀏覽器什么時候進行首次繪制?可以查看本文對瀏覽器首次渲染時間點的探究。
4.圖片的加載和渲染機制
首先我們解答一下上面的問題:圖片的加載與渲染會不會阻塞頁面渲染?答案是圖片的加載和渲染不會影響頁面的渲染。
那么標簽中的圖片和樣式中的圖片的加載和渲染時間是什么樣的呢?
解析HTML【遇到 標簽加載圖片】 —> 構建DOM樹 加載樣式 —> 解析樣式【遇到背景圖片鏈接不加載】 —> 構建樣式規則樹 加載javascript —> 執行javascript代碼 把DOM樹和樣式規則樹匹配構建渲染樹【遍歷DOM樹時加載對應樣式規則上的背景圖片】 計算元素位置進行布局 繪制【開始渲染圖片】
當然把DOM樹和樣式規則樹匹配構建渲染樹時,只會把可見元素和它對應的樣式規則結合一起產出到渲染樹,這就意味有不可見元素,當匹配DOM樹和樣式規則樹時,若發現一個元素的對應的樣式規則上有display:none,瀏覽器會認為該元素是不可見的,因此不會把該元素產出到渲染樹上。
三、性能優化
css優化
1.盡量減少層級
1 #div p.class { 2 color: red; 3 } 4 5 .class { 6 color: red; 7 }
層級減少,意味者匹配時遍歷的dom就少。
關于less嵌套的書寫規范也基于這個道理。
2.使用類選擇器而不是標簽選擇器
減少匹配次數
3.按需加載css
1 (function(){ 2 window.gConfig = window.gConfig || {}; 3 window.gConfig.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); 4 var hClassName; 5 if(window.gConfig.isMobile){ 6 hClassName = ' phone'; 7 8 document.write(''); 9 document.write(''); 10 11 }else{ 12 hClassName = ' pc'; 13 14 document.write(''); 15 document.write(''); 16 17 } 18 var root = document.documentElement; 19 root.className += hClassName ; 20 21 })();
四、 async 與 defer
[來自https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html]
使用
如果腳本是模塊化的并且不依賴于任何腳本,請使用async。
如果該腳本依賴于另一個腳本或由另一個腳本所依賴,則使用defer。
五、減少資源請求
瀏覽器的并發數量有限,所以為了減少瀏覽器因為優先加載很多不必要資源,以及網絡請求和響應時間帶來的頁面渲染阻塞時間,我們首先應該想到的是減少頁面加載的資源,能夠盡量用壓縮合并,懶加載等方法減少頁面的資源請求。
延遲加載圖像
盡管圖片的加載和渲染不會影響頁面渲染,但是為了盡可能地優先展示首屏圖片和減少資源請求數量,我們需要對圖片做懶加載。
1 document.addEventListener("DOMContentLoaded", function() { 2 let lazyImages = [].slice.call(document.querySelectorAll("img.lazy")); 3 let active = false; 4 5 const lazyLoad = function() { 6 if (active === false) { 7 active = true; 8 9 setTimeout(function() { 10 lazyImages.forEach(function(lazyImage) { 11 if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") { 12 lazyImage.src = lazyImage.dataset.src; 13 lazyImage.srcset = lazyImage.dataset.srcset; 14 lazyImage.classList.remove("lazy"); 15 16 lazyImages = lazyImages.filter(function(image) { 17 return image !== lazyImage; 18 }); 19 20 if (lazyImages.length === 0) { 21 document.removeEventListener("scroll", lazyLoad); 22 window.removeEventListener("resize", lazyLoad); 23 window.removeEventListener("orientationchange", lazyLoad); 24 } 25 } 26 }); 27 28 active = false; 29 }, 200); 30 } 31 }; 32 33 document.addEventListener("scroll", lazyLoad); 34 window.addEventListener("resize", lazyLoad); 35 window.addEventListener("orientationchange", lazyLoad); 36 });
詳情參考延遲加載圖像和視頻
六、大促活動實踐
6.1懶加載與異步加載
懶加載與異步加載是大促活動性能優化的主要手段,直白的說就是把用戶不需要或者不會立即看到的頁面數據與內容全都挪到頁面首屏渲染完成之后去加載,極限減小頁面首屏渲染的數據加載量與js,css執行帶來的性能損耗。
6.1.1 導航下拉的異步加載
導航的下拉內容是一塊結構非常復雜的html片段,如果直接加載,瀏覽器渲染的時間會拖慢頁面整體的加載時間:
所有我們需要通過異步加載方式來獲取這段html片段,等頁面首屏渲染結束后再添加到頁面上,大致的代碼如下:
1 $.ajax({ 2 url: url, async: false, timeout: 10000, 3 success: function (data) { 4 container.innerHTML = data; 5 var appendHtml = $('
'); 6 var tempHtml = '