不簡單的前端性能優化

本文主要介紹「關鍵渲染路徑」與「網絡」兩個方面的性能優化並提供demo,篇幅較長建議電腦觀看。php

前端優化的方面太多,本文介紹的僅僅是其中的一部分,力求涵蓋「關鍵渲染路徑」的方方面面,及一些不常被提到的「網絡優化」部分。css

測試環境如無特殊說明均爲Chrome 57html


渲染頁面過程

瀏覽器從打開一個URL到渲染完頁面共有:前端

  • 下載HTML文檔html5

  • 下載HTML文檔中的csswebpack

  • 下載Js文件git

  • 執行js腳本github

  • 下載其餘資源web

  • 經過HTML文檔構建DOM(Parse HTML)瀏覽器

  • 經過CSS文件構建CSSOM(Parse CSS)

  • 經過DOM與CSSOM計算render tree

  • 根據render tree進行繪製,計算各個元素位置與大小(Layout)

  • 對頁面進行上色,渲染爲最終顯示的像素(Paint)

第一次完成Paint稱爲「初次渲染」,這時候用戶就能看到render tree裏面的東西了。而完成初次渲染的過程稱爲「關鍵渲染路徑」,關鍵渲染路徑上須要加載的資源叫作「關鍵資源」

這個過程不少很複雜,其中的依賴關係也很複雜,筆者嘗試畫圖來表示,可是實在是沒畫出來,因此仍是用文字來表述吧:

  • 引入的資源,哪怕被阻塞(好比被js腳本阻塞後續link標籤),瀏覽器依舊會智能的預先加載它們(可是不執行)

  • 「CSS文件的加載」會阻塞「Js文件執行」。若CSS引用在Js文件以前,「加載CSS文件」會阻塞「Js文件執行」。即CSS文件未加載解析完成前,js文件不會獲得執行。由於js有可能會修改CSSOM。帶有async和defer屬性的script不受限制。

  • Parse HTML的解析是增量的,所以瀏覽器能夠邊下載HTML邊構建DOM樹

  • 「CSS文件的加載」會阻塞「Layout」。若頁面有正在加載的CSS文件,在CSS文件加載完以前,瀏覽器不會對頁面進行Layout,這是爲了防止樣式突變帶來的抖動

  • 「加載Js文件」會阻塞「Parse HTML」,這個估計你們都知道了,由於js能夠經過document.write修改HTML文檔流

  • 「Js文件執行」會幾乎會阻塞全部東西,包括Layout

比較有意思的是,字體的加載會阻塞局部的渲染。若某一段文本的字體使用了一個還沒有加載完的字體,這段文本則先不會被Paint,直到字體加載完或者超過某個時間(一般是3秒)文本纔會忽然顯示。

瀏覽器爲了不FOUT(Flash Of Unstyled Text),會儘可能等待字體加載完成後,再顯示應用了該字體的內容。只有當字體超過一段時間仍未加載成功時,瀏覽器纔會降級使用系統字體。每一個瀏覽器都規定了本身的超時時間(Chrome是3秒)。但這也帶來了FOIT(Flash Of Invisible Text)問題。內容沒法儘快地被展現,致使空白

一些Demo來解釋瀏覽器渲染流程

CSS會阻塞Layout:Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css" />
    <!-- 這個css文件會加載3秒鐘,在這個css加載完成前瀏覽器不會layout -->
    <link rel="stylesheet" href="../conn/sleep.php?sleep=3&content=h2{color:red;}" />
    <title>Title</title>
</head>
<body>
    <h1>Hello</h1>
    <h2>World</h2>
</body>
</html>

CSS會阻塞Js執行:Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css" />
    <!-- 這個css文件會加載3秒鐘 -->
    <link rel="stylesheet" href="../conn/sleep.php?sleep=3&content=h2{color:red;}" />
    <script>
        // 這段js會等待css加載完纔會運行
        alert('js is run!');
    </script>
    <title>Title</title>
</head>
<body>
    <h1>Hello</h1>
    <h2>World</h2>
</body>
</html>

Js執行會阻塞關鍵渲染路徑,哪怕是defer仍是async:Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script>
        function sleep(ms){
            var ts =+new Date;
            while(true){
                if(+new Date -ts >=ms) break;
            }
            return +new Date -ts;
        }
    </script>
    <!-- 這個css文件會加載2秒鐘,因此會在js文件以後加載完 -->
    <link rel="stylesheet" href="../conn/sleep.php?sleep=2&content=h2{color:red;}" />
    <!-- 這個js文件會瞬間加載完,可是會運行3秒鐘 -->
    <script defer src="run3s.js"></script>
    <!-- 這個js文件會瞬間加載完,可是會運行2秒鐘 -->
    <script async src="run2s.js"></script>
    <title>Title</title>
</head>
<body>
    <!-- 打開頁面後5秒鐘纔會顯示,由於js執行會阻塞關鍵渲染路徑 -->
    <h1>Hello</h1>
    <h2>World</h2>
</body>
</html>

Foot會阻塞局部渲染,可是智能的瀏覽器會給他設定一個上限,通常是3秒鐘:Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        @font-face {
            font-family: "test-font";
            src: url("../conn/sleep.php?sleep=5&file=scripts_.ttf");
        }
        h1{
            font-family: "test-font";
        }
    </style>
    <title>Title</title>
</head>
<body>
    <h1>Hello</h1>
    <h2>World</h2>
</body>
</html>

CSS篇優化策略

優化核心概念是:將初次渲染不須要的CSS想辦法剝離出關鍵渲染路徑

若是僅僅是爲了提早初次渲染時間而進行優化,將頁面必備的CSS剝離關鍵渲染路徑而形成樣式突變致使頁面抖動,則得不償失了

使用link/style的media屬性

對某些媒體查詢條件觸發後才使用的css,能夠在link標籤中加入media屬性,以下:

<link rel="stylesheet" href="index_print.css" media="print">

此樣式表仍會加載。當瀏覽器環境不匹配媒體查詢條件時,該樣式表不會阻塞渲染。咱們可針對不一樣媒體環境拆分CSS文件,併爲link標籤添加媒體查詢,避免爲了加載非關鍵CSS資源,而阻塞初次渲染

使用DOM API添加CSS

可使用js代碼來添加css

var style = document.createElement('link');
style.rel = 'stylesheet';
style.href = 'index.css';
document.head.appendChild(style);

使用resoure hint規範的preload

將link標籤的rel屬性設置爲preload,瀏覽器遇到遇到標記爲preload的link時,會開始加載它,可是因爲rel不是stylesheet,所以不會阻塞渲染。

<link rel="preload" href="index_print.css" as="style" onload="this.rel='stylesheet'">

而後在適當的時候,在rel改成stylesheet,便可應用此樣式。

可是這個屬性兼容性比較差,詳細能夠參考這裏。不過有一個polyfill能夠用loadCSS,原理是經過DOM API插入樣式資源。

這個屬性的使用情景有些偏,也多是我理解問題:

當使用preload引入css文件時,實際上證實這個頁面根本不須要這個css,它有多是打印樣式,或者是響應式網站的另外一套css代碼。可是,使用preload屬性,瀏覽器反而會預先加載它,也就是說,在window.onload以前,用戶將耗費了網絡資源在加載一個暫時不須要的樣式。網絡資源不多是無限的,也就是說這個css會佔用頁面其餘資源好比圖片的網絡資源。

詢問瓜瓜老師本人後,瓜瓜老師說:

舉個例子。第三屏有個廣告版,它的樣式

這樣確實這個css的緊急程度就介於關鍵渲染路徑的css與頁面圖片之間了,不過貌似這個情景很受限。

JS篇優化策略

使用defer延遲腳本執行

當script標籤擁有defer屬性時,該腳本會被推遲到整個HTML文檔解析完後,再開始執行。所以將腳本放在head中,能夠提前瀏覽器對腳本文件的加載,可是卻不會阻塞parse HTML。

<script src="index.js" defer></script>
<!-- 百度統計代碼 -->
<script src="tongji.js" defer></script>

注意,defer的腳本不會被css阻塞,parse HTML完成後當即執行,可是有可能會阻塞關鍵渲染路徑。爲何說有可能呢,假如腳本文件在render tree生成前加載完畢,則會開始執行,執行過程當中會阻塞關鍵渲染路徑。請參考這個Demo

被defer的腳本,在執行時會嚴格按照在HTML文檔中出現的順序執行,可是實際上貌似不是這樣,js文件先後文件如有依賴需慎重使用。

使用async延遲腳本執行

和defer相似,只是當js加載完後立刻執行,而不在意parse HTML是否完成,所以假如腳本比css先加載完,也會阻塞關鍵渲染路徑。

<script src="index.js" defer></script>
<!-- 百度統計代碼 -->
<script src="tongji.js" defer></script>

使用DOM API

據筆者所知,這是惟一一種100%不會阻塞關鍵渲染路徑的js腳本加載方式。經過DOM API引入的js腳本會等到頁面Layout和Paint後再開始執行,不論你將載入js文件的代碼放在head中仍是body後面亦是如此。

其餘的優化

使用Web Font Loader加載字體

若不想讓字體阻塞局部渲染,可以使用Web Font Loader

網絡優化篇

網絡優化和CSS優化策略相同,儘量讓關鍵資源提早加載完,因此優化時儘可能將如下指標壓縮到最低:

  • 關鍵資源數

  • 關鍵資源體積

  • 關鍵資源網絡來回數

固然,若是你的項目使用了先進的SPDY或HTTP/2,下面的方法可能並不適用。

優化關鍵資源數

RFC2616規定同域名同時只能有 2 個鏈接(RFC7230 中無限制),而​​​​​​​現代瀏覽器通常容許同域6個併發鏈接。所以,當頁面中有許多須要外鏈的資源(script、link等),瀏覽器最多在每一個域同時併發下載6個。

每個請求,若使用域名,則須要額外增長一次DNS查詢時間(若緩存未過時會命中緩存),所以一個網站過多的使用不一樣域名的資源會額外增長DNS查詢開銷,這點在移動端很是明顯。

固然,每一個請求創建根據TCP協議規定,還須要先進行3次捂手才能夠創建連接。

合併請求

儘量的合併請求,減小網絡請求數。這一點可能在其餘性能優化文章都說爛了:

  • 小圖片轉base64

  • 合併打包CSS、JS文件

如今的比較流行的webpack就很是擅長作這種事情

適度使用內聯CSS和Js

使用內聯的CSS和JS當然能夠減小請求,可是使用內聯也意味着你的CSS和JS將不會再被瀏覽器緩存,所以要適度的使用內聯,內聯不是萬能的。

從HTTP協議下手

最佳方案確定是過渡到HTTP/2無疑,可是如今HTTP/2的支持並不算太好,並且各大瀏覽器僅支持TLS下實現的HTTP/2(說白了就是HTTPS),使得HTTP/2的使用存在許些限制。

若是沒有HTTP/2,或許能夠:

  • 使用Keep-Alive能夠規避TCP三次握手的時間

  • 使用Transfer-Encoding:chunked分塊輸出文件,還記得parse HTML的過程是增量的嗎?若瀏覽器能夠邊下載HTML文件邊解析,豈不美哉?

  • 減小重定向,這個看上去理所固然可是實際上卻很容易被忽略

適度使用域名散列

瀏覽器同域並行下載數量有限,因此只要多創建幾個二級域名就行了,而後合理的分配各個資源就行了。

假如因爲某些不可抗拒緣由,關鍵資源數是12個,那麼只要創建2個二級域名分別分配給其中的12個資源,瀏覽器會同時並行下載它們了。

不過,使用域名散列要適度,每個域名都須要額外的增長一次DNS查詢時間。固然,DNS自己也有緩存,或許適當的增長DNS TTL時間也是個不錯的主意。

壓縮關鍵資源體積

對於js、css文件,如今網上現成的壓縮工具一堆,並且應用十分普遍,相信你們都知道了,這裏就很少說了。

說到壓縮,服務器開啓必定的壓縮策略(如gzip)是個不錯的主意,效果拔羣,資源大概會壓縮到原有的1/3左右。

圖片壓縮,這個須要知道什麼情境下適合什麼類型的圖片,GIF、JPG、PNG使用情景各不相同,具體能夠參考這篇文章:圖片格式那麼多,哪一種更適合你?

關鍵資源網絡來回數

假如一個頁面須要引入2個CSS才能工做,下面有2種方式

  • 2個均用link引入

  • 1個用link引入,在css中import另外一個css

毫無疑問確定是前者快,由於前者的網絡來回數是1,然後者是2。

所以,儘量將資源加載扁平化,減小關鍵資源網絡來回數是個不錯的主意。

固然,優化時要注意的點也有很多,好比前面提到的瀏覽器同域併發限制等,須要權衡使其不要影響到其餘的致使初次渲染時間延後。

一些無效的優化策略

使用document.write打印link標籤引入css仍會阻塞初次渲染。

引用

奇舞團@瓜瓜老師:

奇舞團@屈屈老師:

W3C規範:

相關文章
相關標籤/搜索