本篇文章主要從兩個方面講解頁面渲染機制,即網絡方面和渲染引擎方面。
網絡
當用戶訪問頁面時,瀏覽器需要獲取用戶請求內容,這個過程主要涉及瀏覽器網絡模塊。
- 用戶在地址欄輸入域名,比如,baidu.com
- DNS(又稱域名解析系統,默認端口號53)協議,通過域名查找IP地址
瀏覽器DNS解析大多時候比較快,且會緩存常用域名的解析值,但是如果網站涉及多域名,在對每一個域名訪問時都需要先解析出IP地址,而我們希望在跳轉或者請求其他域名資源時盡量快,則可以開啟域名預解析,瀏覽器會在空閑時提前解析聲明需要預解析的域名。如下
即,在網頁頭部(head之間)增加rel屬性為”dns-prefetch”的link標簽,並在href中指定想要預解析的域名。
- 向該IP地址發起請求。請求過程如下
客戶端:
HTTP協議生成針對目標Web服務器的HTTP請求報文;
為了方便通信,TCP協議將HTTP報文分割成報文段,把每個報文段可靠地傳給對方;
ARP協議根據IP地址解析出對應的MAC地址,IP協議根據MAC地址傳送報文。這期間可能會經過多個路由器,IP協議自動中轉,直到找到目的MAC;
服務器端:
TCP協議從客戶端接收到報文段,按序號以原來的順序重組請求報文;
HTTP協議對Web服務器請求的內容進行處理。此時,服務器知道了客戶端想要瀏覽 baidu.com 這個頁面了;
- 瀏覽器獲得並解析服務器的返回內容(HTTP Response)。響應過程如下
服務器端:
客戶端:
- 瀏覽器加載HTML文件以及文件內包含的外部引用文件以及圖片,多媒體等資源。
渲染搜索引擎(關鍵渲染路徑)
渲染引擎所做的事情是將請求內容展現給我們,默認支持HTML,XML和圖片類型,對於其他諸如PDF等類型的內容則需要安裝響應插件,但瀏覽器的展示工作流程基本是一樣的。
通過網絡模塊加載到HTML文件后,渲染引擎渲染流程如下
- 從 Head 標簽開始逐行解析HTML代碼,遇到 link 標簽又會向服務器請求加載CSS文件,這個過程是異步加載。如果有多個CSS文件,會同時加載。
- 如果遇到 script 標簽或者 js 文件,會立即執行,並且這個過程是異步的。
不同於CSS文件,js 是同步加載。即執行js文件時,瀏覽器不會做其他事情,只有js代碼執行結束后,才會繼續開始渲染頁面。為了防止出現“空白頁”現象,應該把 js 放到頁面底部,也就是</body>標簽前。
- 然后到 body 標簽開始渲染頁面,按照從上到下的順序依次渲染dom節點。如果遇到 img 標簽,會異步向服務器發送請求加載圖片文件,瀏覽器會繼續渲染頁面,因為圖片加載是異步的~
- 如果遇到了dom節點的變化,元素尺寸變化,瀏覽器不得不回頭重新渲染這部分代碼。
以上就是本篇文章介紹的瀏覽器頁面渲染機制了,下面簡單介紹下如何提升頁面的渲染效率~
影響頁面渲染速率
- 回流(reflow)
當瀏覽器發現頁面某個部分發生了變化影響了布局,需要倒回去重新渲染,該過程稱為回流。
- 重繪(repaint)
如果只是改變了某個元素的邊框顏色、字體顏色、背景色等不影響它周圍或內部布局的屬性,將只會影響瀏覽器的重繪。
提升頁面渲染速率
- CSS注意事項
用CSS動畫替代js模擬動畫的好處是:不占用js主線程;可以利用硬件加速;瀏覽器可對動畫做優化。但CSS動畫有時會出現卡頓現象
使用CSS3動畫造成頁面的不流暢和卡頓問題,其潛在原因往往還是頁面的回流和重繪,減少頁面動畫元素對其他元素的影響是提高性能的根本方向。
1、設置動畫元素 position 樣式為absolute或 fixed,可避免動畫的進行對頁面其它元素造成影響,導致其重繪和重排的發生;
2、避免使用margin,top,left,width,height等屬性執行動畫,用 transform 進行替代;
下面有一個CSS動畫,動畫開始后,你會隱約感覺到動畫不是那么流暢,即使使用電腦上的瀏覽器也會有些卡頓,更不要提在移動端達到 60 fps的流暢效果了。。。
.div {
animation: run-around 4s infinite;
}
@keyframes run-around {
0% {
top: 0;
left: 0;
}
25% {
top: 0;
left:200px;
}
50% {
top: 200px;
left: 200px;
}
75% {
top: 200px;
left: 0px;
}
}
為了解決這個問題,我們可以使用CSS transform中的 translate() 來代替 top 和 left。
.div {
animation: run-around 4s infinite;
}
@keyframes run-around {
0% {
transform: translate(0,0);
}
25% {
transform: translate(200px,0);
}
50% {
transform: translate(200px,200px);
}
75% {
transform: translate(200px,0);
}
}
現在動畫看起來會好很多。因為transform屬性不會出發瀏覽器的 repaint,而 top 和 left 會一直觸發 repaint。為什么 transform 沒有觸發 repaint 呢?因為,transform 動畫由GPU控制,支持硬件加速,並不需要軟件方面的渲染。
GPU詳細介紹,可以參考 https://www.jianshu.com/p/d1e16a2e88c1 這篇文章。
- JS注意事項
解決js同步加載問題(有下面三種方式)
1、將js文件放在頁面底部,即</body>標簽之前。因為html文件默認是按照順序從上到下依次加載的,這樣就可以先渲染dom節點,再加載js
2、使用 H5 的async屬性,用法和特點如下
<script src = "test.js" anysc></script>
//加載腳本時不阻塞頁面渲染
//使用這個屬性的腳本中不能調用document.write方法
//可以只寫屬性名,不寫屬性值。寫法如上
//H5新增屬性
//腳本在下載后立即執行,同時會在window的load事件之前執行,所以有可能出現腳本執行順序被打亂的情況
3、使用HTML的defer屬性,用法和特點如下(前三點和anysc相同)
<script src = "test.js" defer></script>
//加載腳本時不阻塞頁面渲染
//使用這個屬性的腳本中不能調用document.write方法
//可以只寫屬性名,不寫屬性值。寫法如上
//H4屬性
//腳本在頁面解析完之后,按照原本的順序執行,同時會在document的DOMContentLoaded之前執行
避免頻繁操作DOM元素
1、當需要很多的插入操作和改動,使用下面的代碼會很有問題
var ul = document.getElementById("ul");
for (var i = 0;i < 20;i++){
var li = document.creatElement("li");
ul.appendChild(li);
}
由於每一次對文檔的插入都會引起重新渲染(計算元素的尺寸、顯示背景、內容等),所以進行多次插入操作使得瀏覽器發生了多次渲染,效率比較低。這是我們提倡減少頁面的渲染來提高DOM操作的效率的原因。
createDocumentFragment()方法,是用來創建一個虛擬的節點對象,或者說,是用來創建文檔碎片節點,它可以包含各種類型的節點,在創建之初是空的。它有一個很實用的特點,當請求把一個createDocumentFragment 節點插入文檔樹時,插入的不是createDocumentFragment 自身,而是它所有的子孫節點。這個特性使得createDocumentFragment 成了占位符,暫時存放那些一次插入文檔的節點。
另外,當需要添加多個dom元素時,如果先將這些元素添加到createDocumentFragment 中,再統一將createDocumentFragment 添加到頁面。因為文檔片段存在於內存中,並不在DOM中,所以將子元素插入文檔片段中不會引起回流(對元素位置和幾何上的計算),因此,使用DocumentFragment可以起到性能優化的作用。可以將上面的代碼改成下面的
var ul = documen.getElementById("ul");
var fragment = document.createDocumentFragment();
for (var i = 0;i < 20;i++){
var li = document.createElement("li");
li.innerHtml =" index: " + i;
fragment.appendChild(li);
}
ul.appendChild(fragment);
關於 createDocumentFragment() 方法更多介紹,請讀者自行查閱~
2、設置DOM元素的display屬性為none再操作該元素
var myElement = document.getElementById('myElement');
myElement.style.display = 'none';
…… //一些基於myElement的大量DOM操作
myElement.style.display = 'block';
3、復制DOM元素到內存中再對其進行操作
var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
…… //一些基於clone的大量操作
old.parentNode.replaceChild(clone,old);
4、用局部變量緩存樣式信息從而避免頻繁獲取DOM數據
比如訪問一個元素的offsetWidth屬性時,瀏覽器需要重新計算(重新布局),然后才能返回最新的值,如果這個動作發生在一個很大的循環中,那么瀏覽器就不得不進行多次重新布局,這可能會產生嚴重的性能問題。正確的做法是,先將這個值讀出來,然后緩存在一個變量上(觸發一次重新布局)。以便后續使用
//一般用法
for (var i = 0;i < paragraphs.length;i++) {
paragraphs.style.width = box.offsetWidth + 'px';
}
//優化性能的用法
var width = box.offsetWidth;
for (i = 0;i < paragraphs.length;i++){
paragraphs[i].style.width = width + 'px';
}
5、合並多次DOM操作
//一般用法
var left = 10;top = 10;
el.style.top = top;
el.style.left = left;
//優化性能寫法
el.style.cssText += ";left: " + left + "px; top: " + top + "px;";
- 其他
除了CSS 和 JS 的改進能夠提升頁面渲染速率,還有其他方面的改進同樣能夠提升頁面渲染速率。
1、資源壓縮與合並。
HTML代碼壓縮:壓縮在文本中有意義,而在HTML中不需要的字符。比如,空格、制表符、換行符,還有一些其他意義的字符,如HTML注釋也可以被壓縮。
CSS代碼壓縮:刪除無效的代碼和css語義合並。
JS的壓縮和紊亂:使用在線網站壓縮、使用html-minifier工具、使用uglifyjs2進行壓縮。
文件合並:將多個js/css小文件合並為一個文件,減少網絡請求次數。
注:css壓縮與js的壓縮和紊亂比html壓縮收益要大的多,同時css代碼和js代碼比html代碼多的多。所以,css與js代碼壓縮非常有必要!
2、瀏覽器緩存
緩存作用:對於web應用來說,緩存是提升頁面性能同時減少服務器壓力的利器。
強緩存:不會向服務器發送請求,直接從緩存中讀取資源,在Chrome控制台的network選項中可以看到該請求的狀態碼是 200,但是 size 的標識為 from dist cache 或者 from memory cache
response header:response header里的過期時間,瀏覽器再次加載該資源時,如果在有效時間內,則使用強緩存。
Last-Modified 和 If-Modified-Since:二者都是記錄頁面最后修改時間的 HTTP 頭信息,Last-Modified 是由服務器往客戶端發送的HTTP, If-Modified-Since 是客戶端往服務器端發送的頭。
再次請求本地緩存的 cache 頁面時,客戶端會通過 If-Modified-Since 頭先將服務器端發過來的 Last-Modified 最后修改時間戳發送回去,這是為了讓服務器端進行驗證,通過這個時間戳判斷客戶端的頁面是否是最新的。如果不是最新的,則返回新的內容,如果是最新的,則返回 304 告訴客戶端其本地 cache 的頁面時最新的,於是客戶端就可以直接從本地加載頁面了,這樣在網絡上傳輸的數據就會大大減少,同時也減輕了服務器端的負擔。而在一些ajax應用中,要求回獲取的數據永雲是最新的,而不是讀取緩存中的數據,做這樣的設置是很有必要的。
3、CDN預解析
CDN服務提供商會有全國各個省份部署節點, 將網站靜態資源部署到CDN后, 用戶在訪問頁面時, CDN靜態資源會從就近的CDN節點上加載資源. 當請求至達CDN節點后, 節點會判斷資源是緩存是否有效, 若有效, 直接返回給用戶, 若無效, 會從CDN服務器加載最新的資源返回給用戶同時將資源保存一份到該CDN節點上, 以便后續的訪問用戶使用. 因此, 只在該地區有一個用戶先加載了資源, 在CDN中建立了緩存, 該地區的其他用戶都能受益。
4、DNS預解析