前端工程師必備:從瀏覽器的渲染到性能優化

摘要:本文主要講談及瀏覽器的渲染原理、流程以及相關的性能問題。

問題前瞻

1. 爲何css須要放在頭部?
2. js爲何要放在body後面?
3. 圖片的加載和渲染會阻塞頁面DOM構建嗎?
4. dom解析完纔出現頁面嗎?
5. 首屏時間根據什麼來斷定?

瀏覽器渲染

1.瀏覽器渲染圖解

[來自google開發者文檔]javascript

瀏覽器渲染頁面主要經歷了下面的步驟:css

1.處理 HTML 標記並構建 DOM 樹。
2.處理 CSS 標記並構建 CSSOM 樹。
3.將 DOM 與 CSSOM 合併成一個渲染樹。
4.根據渲染樹來佈局,以計算每一個節點的幾何信息。
5.將各個節點繪製到屏幕上。

爲構建渲染樹,瀏覽器大致上完成了下列工做:html

從 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{}

從左向右的匹配規則java

從右向左的匹配規則jquery

若是css從左向右解析,意味着咱們須要遍歷更多的節點。無論樣式規則寫得多細緻,每個dom結點仍然須要遍歷,由於整個style rules還會有其它公共樣式影響。若是從右向左解析,由於子元素只有一個父元素,因此可以很快定位出當前dom符不符合樣式規則。git

3.js加載和執行機制

首先明確一點,咱們能夠經過js去修改網頁的內容,樣式和交互等,這一意味着js會影響頁面的dom結構,若是js和dom構建並行執行,那麼很容易會出現衝突,因此js在執行時必然會阻塞dom和cssom的構建過程,不管是外部js仍是內聯腳本。es6

js的位置是否影響dom解析?web

首先咱們爲何提倡把js放在body標籤的後面去加載,由於從demo上看不管是放在head仍是放在body後加載js,頁面domcontentload的時間都是同樣的:ajax

咱們從圖中能夠看出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 >' + '<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.總結

前端性能優化的方法手段並不只限於文章陳述,官網前端團隊還會在前端性能優化的道路上學習更多,探索更多,將華爲雲官網頁面的加載性能作到極致!

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索