【華爲雲技術分享】前端工程師必備:從瀏覽器的渲染到性能優化

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

 

問題前瞻

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>

從左向右的匹配規則

從右向左的匹配規則

如果css從左向右解析,意味着我們需要遍歷更多的節點。不管樣式規則寫得多細緻,每一個dom結點仍然需要遍歷,因爲整個style rules還會有其它公共樣式影響。如果從右向左解析,因爲子元素只有一個父元素,所以能夠很快定位出當前dom符不符合樣式規則。

3.js加載和執行機制

首先明確一點,我們可以通過js去修改網頁的內容,樣式和交互等,這一意味着js會影響頁面的dom結構,如果jsdom構建並行執行,那麼很容易會出現衝突,所以js在執行時必然會阻塞domcssom的構建過程,不論是外部js還是內聯腳本。

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

首先我們爲什麼提倡把js放在body標籤的後面去加載,因爲從demo上看無論是放在head還是放在body後加載js,頁面domcontentload的時間都是一樣的:

我們從圖中可以看出js的加載和執行是阻塞dom解析的,但是因爲頁面並不是一次就渲染完成,所以我們需要做的是儘量讓用戶看到首屏的部分被渲染出來,js放在頭部,則頁面的內容區域還沒有解析到就被阻塞了,導致用戶看到的是白屏,而js放在body後面,儘管此時頁面dom仍然沒有解析完成,但是已經渲染出一部分樓層了,這也是爲什麼我們比較看重頁面的首屏時間。

只有DOMCSSOM樹構建好後併合併成渲染樹才能開始繪製頁面圖形,那是不是把整個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 懶加載與異步加載

懶加載與異步加載是大促活動性能優化的主要手段,直白的說就是把用戶不需要或者不會立即看到的頁面數據與內容全都挪到頁面首屏渲染完成之後去加載,極限減小頁面首屏渲染的數據加載量與jscss執行帶來的性能損耗。

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

加載方式

異常上報

raven.min.js + pmp.js + 觸發異常上報邏輯

body後順序加載

jquery

jquery.min.js

頭部加載

基礎功能

jquery.base64.js + agrid.js

頭部加載

微信分享

wxshare.min.js + Weixinshare.js

頭部加載

   

這部分js存在問題是分散在pep的各個資產庫路徑維護,有些壓縮了,有些沒有壓縮,js的加載也基本是順序執行,所以我們對這個部分的jscss資源進行了一個整合,進行的操作是遷移合併壓縮

建立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之後去加載的,這些我們就可以把頁面加載時不需要執行的jscss全部移到頁面渲染完成後去加載,少了這部分的js邏輯執行時的阻塞,頁面首屏渲染的時間也會大大降低。

通過cui套件中的feloader插件,我們可以比較便捷的控制jscss加載的時機:

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)">

優化效果

頁面

華爲雲雙十一頁面(s

阿里雲雙十一頁面(s

對比

PC端首屏加載

0.398s

0.601s

-33.78%

PC端完全加載

5.195

15.996

-67.52%

移動端首屏加載

1.234s

3.238s

-61.89%

移動端完全加載

2.778s

5.447s

-49.00%

3.總結

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

 

點擊這裏→瞭解更多精彩內容

 

相關推薦


前端常用60餘種工具方法(上)

三大前端技術(React,Vue,Angular)探密(上)

前端快速建⽴Mock App

【一統江湖的大前端(8)】matter.js 經典物理

如何度量前端項目研發效率與質量(上)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章