前端工程師必備:從瀏覽器的渲染到效能優化
摘要:本文主要講談及瀏覽器的渲染原理、流程以及相關的效能問題。
問題前瞻
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 <div id="div1"> 2 <div class="a"> 3 <div class="b"> 4 ... 5 </div> 6 <div class="c"> 7 <div class="d"> 8 ... 9 </div> 10 <div class="e"> 11 ... 12 </div> 13 </div> 14 </div> 15 <div class="f"> 16 <div class="c"> 17 <div class="d"> 18 ... 19 </div> 20 </div> 21 </div> 22 </div>
1 #div1 .c .d {} 2 .f .c .d {} 3 .a .c .e {} 4 #div1 .f {} 5 .c .d{}
從左向右的匹配規則
從右向左的匹配規則
如果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('<link rel="stylesheet" href="https://res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/m/index.css" />'); 9 document.write('<link rel="preload" href="//res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/m/index.js" crossorigin="anonymous" as="script" />'); 10 11 }else{ 12 hClassName = ' pc'; 13 14 document.write('<link rel="stylesheet" href="https://res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/pc/index.css" />'); 15 document.write('<link rel="preload" href="//res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/pc/index.js" crossorigin="anonymous" as="script" />'); 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 });
詳情參考延遲載入影象和視訊
大促活動實踐
2.1 懶載入與非同步載入
懶載入與非同步載入是大促活動效能優化的主要手段,直白的說就是把使用者不需要或者不會立即看到的頁面資料與內容全都挪到頁面首屏渲染完成之後去載入,極限減小頁面首屏渲染的資料載入量與js,css執行帶來的效能損耗。
2.1.1 導航下拉的非同步載入
導航的下拉內容是一塊結構非常複雜的html片段,如果直接載入,瀏覽器渲染的時間會拖慢頁面整體的載入時間:
所有我們需要通過非同步載入方式來獲取這段html片段,等頁面首屏渲染結束後再新增到頁面上,大致的程式碼如下:
1 $.ajax({ 2 url: url, async: false, timeout: 10000, 3 success: function (data) { 4 container.innerHTML = data; 5 var appendHtml = $('<div class="footer-wrapper">' + container.querySelector('#footer').innerHTML + '</div>'); 6 var tempHtml = '<div style="display:none;">' + '<script type="text/html" id="header-lazyload-html-drop" class="header-lazyload-html" data-holder="#holder-drop">' + appendHtml.find('#header-lazyload-html-drop').html() + '<\/script><script type="text/html" id="header-lazyload-html-mbnav" class="header-lazyload-html" data-holder="#holder-mbnav">' + appendHtml.find('#header-lazyload-html-mbnav').html() + '<\/script></div>'; 7 $('#footer').append(tempHtml); 8 feloader.onLoad(function () { 9 feloader.use('@cloud/common-resource/header', function () { 10 }); 11 $('#footer').css('display', 'block'); 12 }); 13 }, 14 error: function (XMLHttpRequest, textStatus, errorThrown) { 15 console.log(XMLHttpRequest.status, XMLHttpRequest.readyState, textStatus); 16 }, 17 });
2.1.2 圖片懶載入
官網的cui套件中已經有lazyload的外掛支援圖片懶載入,使用方法頁非常簡單:
1 <div class="list"> 2 <img class="lazyload" data-src="http://www.placehold.it/375x200/eee/444/1" src="佔位圖片URL" /> 3 <img class="lazyload" data-src="http://www.placehold.it/375x200/eee/444/2" src="佔位圖片URL" /> 4 <img class="lazyload" data-src="http://www.placehold.it/375x200/eee/444/3" src="佔位圖片URL" /> 5 <div class="lazyload" data-src="http://www.placehold.it/375x200/eee/444/3"></div> 6 ... 7 </div>
從程式碼我們差不多可以猜出圖片懶載入的原理,其實就是我們通過覆蓋img標籤src屬性,使得img標籤開始載入時由於沒有src的具體圖片地址而不去載入圖片,等到重要資源載入完之後,通過監聽onload的時間或者滾動條的滾動時機再去重寫對應標籤的src值來達到圖片懶載入:
1 /** 2 * load image 3 * @param {HTMLElement} el - the image element 4 * @private 5 */ 6 _load(el) { 7 let source = el.getAttribute(ATTR_IMAGE_URL); 8 if (source) { 9 let processor = this._config.processor; 10 if (processor) { 11 source = processor(source, el); 12 } 13 14 el.addEventListener('load', () => { 15 el.classList.remove(CLASSNAME); 16 }); 17 // 判斷是否是什麼元素 18 if (el.tagName === 'IMG') { 19 el.src = source; 20 } else { 21 // 判斷source是不是一個類名,如果是類名的話,則加到class裡面去 22 if (/^[A-Za-z0-9_-]+$/.test(source)) { 23 el.classList.add(source); 24 } else { 25 let styles = el.getAttribute('style') || ''; 26 styles += `;background-image: url(${source});`; 27 el.setAttribute('style', styles); 28 el.style.backgroundImage = source; // = `background-image: url(${source});`; 29 } 30 } 31 32 el.removeAttribute(ATTR_IMAGE_URL); 33 } 34 }
具體的外掛程式碼大家可以檢視https://git.huawei.com/cnpm/lazyload。
同時官網的頁尾部分也採用了採用其它的載入方式也實現了懶載入的效果,頁尾的圖片都在css中引用,想要延遲載入頁尾圖片就需要延遲載入頁尾的css,但是延遲載入css造成的後果就是頁面載入的一瞬間頁尾會因為樣式確實而顯示錯亂,所以我們可以在css樣式載入前強勢隱藏掉頁尾部分,等css載入完成後,頁尾dom自帶的display:block會自動顯示頁尾。(==因為頁尾的seo特性沒有對其進行懶載入==)
2.1.3 樓層內容的懶載入
基於xtpl自帶的懶載入能力,配合pep定製頁面模板的邏輯,我們可以實現html的懶載入。在頁面初次渲染的時候,只有每個樓層的大體框架和標題等關鍵資訊,如果需要的話可以給預設圖片等佔位,或設定最小高度佔位,防止錨點定位失效。
當頁面滾動到該樓層的位置,js程式碼方會執行,在初始化函式中,對該樓層的html進行載入,渲染,實現樓層圖片和html的懶載入,減少了首屏時間。
具體程式碼如下:
1 <div class="nov-c6-cards j-content"> 2 </div>
1 public render(){ 2 this.$el.find('.j-content').html(new Xtemplate(tpl).render(mockData)) 3 ... 4 }
2.1.4 套餐資料懶載入
套餐資料的載入一直以來都是令人頭疼的,本次雙十一對於套餐指令碼也做了優化,不僅對資料進行了快取,同時也可以在指定的範圍進行套餐資料的渲染——和上述所說的樓層懶載入配合,可以做到未展示的樓層,套餐資料不請求,下拉框不渲染,詢價介面不呼叫,在首屏不出現大量套餐的情況下,可以大大提升首屏載入的效能。
2.2.資源整合
2.2.1.頁頭頁尾資源統一維護
基礎模板的優化涉及到資源的合併,壓縮與非同步載入,dom的延遲載入和圖片的懶載入。首先我們給出官網基礎模板引用的一部分js資源的表格:
這部分js存在問題是分散在pep的各個資產庫路徑維護,有些壓縮了,有些沒有壓縮,js的載入也基本是順序執行,所以我們對這個部分的js和css資源進行了一個整合,進行的操作是遷移,合併,壓縮。
建立common-resource倉庫去統一維護管理頁頭頁尾及公共資原始碼。
2.2.2.合併載入方式相同的基礎功能js並壓縮
common.js
1 import './common/js/AGrid'; 2 import './common/js/jquery.base64'; 3 import './common/js/lang-tips'; 4 import './common/js/setLocaleCookie'; 5 import './common/js/pepDialog';
如上面程式碼,將官網中用的分散的基礎功能js合併成一個common.js,經過伏羲流水線釋出,cui套件會自動將js壓縮,這樣做的效果當然是減少官網頁面請求資源數,減小資源大小。
2.2.3.資源非同步載入
觀察2.2.1中的表格可以發現,官網大部分js都是放在頭部或者是body後順序載入的,這些資源的載入時間必定是在DOMOnLoad之前
這些js都是會阻塞頁面的渲染,導致頁面首屏載入變慢,我們需要做的就是通過之前頭尾資源的整理得出哪些資源是可以在onload之後去載入的,這些我們就可以把頁面載入時不需要執行的js和css全部移到頁面渲染完成後去載入,少了這部分的js邏輯執行時的阻塞,頁面首屏渲染的時間也會大大降低。
通過cui套件中的feloader外掛,我們可以比較便捷的控制js和css載入的時機:
1 feloader.onLoad(function () { 2 feloader.use([ 3 '@cloud/link-to/index', 4 '@cloud/common-resource/uba', 5 '@cloud/common-resource/footer', 6 '@cloud/common-resource/header', 7 '@cloud/common-resource/common', 8 '@cloud/common-resource/prompt.css', 9 '@cloud/common-resource/footer.css', 10 ]); 11 });
下圖可以明顯看到js的載入都轉移到onload之後了:
2.2.4 圖片壓縮
除了對設計給出的圖片有壓縮要求外,我們還通過對一部分不常更新的小圖示圖片進行base64編碼來減少頁面的圖片請求數量。
2.3預解析與預載入
除了延遲載入外,基礎模板還進行了諸如dns預解析,資源預載入的手段來提前解析dns和載入頁面資源。
2.3.1 DNS 預解析
當用戶訪問過官網頁面後,DNS預解析能夠使使用者在訪問雙十一活動頁之前提前進行DNS解析,從而減少雙十一活動頁面的dns解析時間,提高頁面的訪問效能,其實寫法也很簡單:
1 <link rel="dns-prefetch" href="//res.hc-cdn.com"> 2 <link rel="dns-prefetch" href="//res-static1.huaweicloud.com"> 3 <link rel="dns-prefetch" href="//res-static2.huaweicloud.com"> 4 <link rel="dns-prefetch" href="//res-static3.huaweicloud.com">
2.3.2 preload 預載入
活動頁的部分js還使用了preload預載入的方式來提升頁面載入效能,preload的為什麼可以達到這種效果,我們需要看下面這段摘錄:
Preloader 簡介
HTML 解析器在建立 DOM 時如果碰上同步指令碼(synchronous script),解析器會停止建立 DOM,轉而去執行指令碼。所以,如果資源的獲取只發生在解析器建立 DOM時,同步指令碼的介入將使網路處於空置狀態,尤其是對外部指令碼資源來說,當然,頁面內的指令碼有時也會導致延遲。
預載入器(Preloader)的出現就是為了優化這個過程,預載入器通過分析瀏覽器對 HTML 文件的早期解析結果(這一階段叫做“令牌化(tokenization)”),找到可能包含資源的標籤(tag),並將這些資源的 URL 收集起來。令牌化階段的輸出將會送到真正的 HTML 解析器手中,而收集起來的資源 URLs 會和資源型別一起被送到讀取器(fetcher)手中,讀取器會根據這些資源對頁面載入速度的影響進行有次序地載入。
基於以上原理,我們對官網相對重要的js資源進行preload預載入,以使得瀏覽器可以儘快地載入頁面所需的重要資源。
1 <link rel="preload" href="//res.hc-cdn.com/cnpm-feloader/1.0.6/feloader.js" as="script"/> 2 <link rel="preload" href="//polyfill.alicdn.com/polyfill.min.js?features=default,es6" as="script"/> 3 <link rel="preload" href="https://res-static3.huaweicloud.com/content/dam/cloudbu-site/archive/commons/3rdlib/jquery/jquery-1.12.4.min.js" as="script"/> 4 <link rel="preload" href="//res.hc-cdn.com/cnpm-wpk-reporter/1.0.6/wpk-performance.js" as="script"/> 5 6 <link rel="preload" href="//res.hc-cdn.com/cpage-pep-2019nov-promotion/1.1.15/components/activity-banner/images/banner_mb.jpg" as="image" media="(max-width: 767px)">
優化效果
3.總結
前端效能優化的方法手段並不僅限於文章陳述,官網前端團隊還會在前端效能優化的道路上學習更多,探索更多,將華為雲官網頁面的載入效能做到極致!
點選關注,第一時間瞭解華為雲新鮮技