前端性能優(yōu)化之瀏覽器關(guān)鍵渲染路徑

寫在前面

關(guān)于前端性能優(yōu)化的文章非常多,寫瀏覽器關(guān)鍵渲染路徑的也不少,但總是感覺哪里錯(cuò)了或者哪里疏忽了,于是自己寫一篇,同時(shí)也是最近面試的一篇總結(jié)~
下面分別從瀏覽器渲染過程,文檔資源的加載與阻塞,關(guān)鍵渲染路徑及優(yōu)化等三個(gè)角度來看。

瀏覽器渲染過程

面試常被問到 “前端怎樣性能優(yōu)化”,我自己在回答時(shí)總是會按照 “瀏覽器從輸入一個(gè) URL 到頁面顯示經(jīng)歷了什么” 的思路去回答,這樣的回答既顯條理,又不易因緊張而將一些簡單的性能優(yōu)化法忘記。扯回來,瀏覽器渲染可以是這個(gè)題目的最后一個(gè)環(huán)節(jié),先上一張自己畫的圖:

瀏覽器渲染過程

類似的圖也有不少,但大同小異,大概即是瀏覽器首先加載 HTML 文檔,過程中遇到了其他資源會同時(shí)加載并解析其他資源,最后生成 Rendering tree 經(jīng)過 layout 與 paint 顯示到頁面。需要注意的是,html 渲染為增量式渲染,瀏覽器無需等待 html 加載完便開始解析構(gòu)建 DOM。這張圖有助于后面的理解,但不再細(xì)說。

記:哪些操作會觸發(fā) reflow ?哪些會觸發(fā) repaint ?

HTML / CSS / JS 的加載與阻塞

關(guān)于加載

需要明確,.html / .css / .js 包括圖片音頻等其他資源,在加載上幾乎完全并行(幾乎?請看下文),不存在阻塞一說。誠然,我們訪問 example.com/index.html 時(shí)需要解析到 <link rel="stylesheet" href="/style.css"> 才會去加載 style.css,但這并不影響瀏覽器同時(shí)去加載另外的資源。

  1. 加載:漢語詞語,字面意思是增加裝載量?,F(xiàn)多用于計(jì)算機(jī)相關(guān)領(lǐng)域,表示啟動程序時(shí)文件或信息的載入。英譯 “l(fā)oad”。(來自百度百科)
  2. 在前端這里,“加載” 就是下載。
  3. 很多文章將 “加載 (load)” 與 “解析 (parse)” “渲染 (layout+paint)” 混淆,我認(rèn)為是不妥的,本文會一一講到。

資源之間的加載是并行,互不影響的,但是加載也是有限制的,現(xiàn)代瀏覽器對同一域名下的最大并發(fā)連接數(shù)一般是 6,也就是說如果同時(shí)對同一域名發(fā)起超過 6 個(gè)連接,超出的請求就會被阻塞。??吹?js css 等靜態(tài)文件使用 CDN 托管,一個(gè)原因即是通過使用多域名并發(fā)加載,提高加載速度。

但加載需要時(shí)間,現(xiàn)代瀏覽器為了性能考慮,不會無限制的等待資源加載,對資源的加載會有失效時(shí)間。在 Chrome 60 中測試加載 js / css 文件時(shí)最多會等待 4 min,若 4 min 內(nèi)服務(wù)器無任何響應(yīng)數(shù)據(jù)返回,則會觸發(fā)加載錯(cuò)誤,會在 console 輸出 net::ERR_EMPTY_RESPONSE 錯(cuò)誤提示,同時(shí)瀏覽器繼續(xù)解析。但若 4 min 內(nèi)返回部分響應(yīng)數(shù)據(jù),則會繼續(xù)等待完整的響應(yīng)返回(即會突破 4 min 的失效時(shí)間限制,我測試了 13 min 仍然沒有報(bào)錯(cuò)我放棄了,推測瀏覽器沒有對此做進(jìn)一步的時(shí)間限制,這是合理的),這里與服務(wù)器推送技術(shù) long-polling 相似。見下圖:

net::ERR_EMPTY_RESPONS 錯(cuò)誤

關(guān)于阻塞

明確了這點(diǎn),那么資源之間的阻塞是怎樣的?阻塞是指資源加載完之后按照一定的順序解析 (parse) (或 html 文檔的渲染等)(注意這里的順序不是指完全的串行),有順序就有阻塞,下面講到 css 阻塞 html 渲染,js 阻塞 DOM 樹構(gòu)建,css 阻塞 js 的解析,js 阻塞一切等:

  1. css 阻塞 html 渲染 (layout)
    試想,如果 css 不阻塞 html 渲染,那么瀏覽器會先將無樣式的 html 渲染出來,然后突然產(chǎn)生樣式,即 FOUT(Flash Of Unstyled Text)。因此現(xiàn)代瀏覽器中,通過內(nèi)聯(lián) <style>,外聯(lián) <link> 以及
    document.write('<link ...>') 等引入的 css 均會阻塞 html 渲染,但不會影響 html 構(gòu)建 DOM 樹。
    需要注意的是,這與 css 在文檔中引入位置無關(guān),考慮下面的代碼:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    
    <body>
        <h1>hello, zphhhhh</h1>
        <link rel="stylesheet" href="/css/style.css">
    </body>
    
    </html>
    

    上面代碼若 css 文件加載需要 2s,文檔會在 2s 后才顯示出來,但 DOM 樹早已構(gòu)建完畢,就等 CSSOM 了。但也存在 css 不阻塞的情況,比如

    • 媒體查詢不符合時(shí),會同時(shí)加載但不會阻塞 html 渲染:
      <link rel="stylesheet" href="index_print.css" media="print">
      
    • 使用 DOM API 動態(tài)生成 link:
      document.createElement('link');
      
    • CSS preload,是 Resource Hints 規(guī)范,兼容性問題不再細(xì)說,可自行了解。
      <link rel="preload" href="index.css" as="style" onload="this.rel='stylesheet'">
      
  2. js 阻塞 DOM 樹構(gòu)建(沒有 DOM 樹就沒有 HTML 渲染)
    js 可以通過 document.write 修改 HTML 文檔流,因此在執(zhí)行 js 時(shí),瀏覽器會停止構(gòu)建 DOM,但由于瀏覽器的增量構(gòu)建,瀏覽器可能會渲染出一部分 DOM( js 之前),考慮下面的代碼:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    
    <body>
        <h1>hello,</h1>
        <script src="/main.js"></script>
        <h1>zphhhhh</h1>
    </body>
    
    </html>
    

    上面代碼若 js 文件加載需要 2s,文檔會先顯示 hello,,2s 后再顯示 zphhhhh。但也存在 js 不阻塞 DOM 構(gòu)建的情況,可以:

    • 加入 async / defer 屬性(只是有可能不阻塞),聲明 js 中沒有使用 document.write(),二者區(qū)別在于,聲明 async 會在 js 加載完后立即解析執(zhí)行,而聲明 defer 在 js 加載完仍需等待 DOM 樹構(gòu)建完成(即推遲執(zhí)行),看圖就明白:

      async VS defer

      如下代碼:

      <script async src="./main.js"></script>
      或
      <script defer src="./main.js"></script>
      

      但這也意味著 js 中真的不能使用 document.write() 了,強(qiáng)行使用會有提示,且沒有生效,但同一文件中其他代碼仍可正常執(zhí)行:

      Failed to execute 'write' on 'Document': It isn't possible to write into a document from an asynchronously-loaded external script unless it is explicitly opened.
      
    • 使用 DOM API 動態(tài)生成 script

      document.createElement('script');
      
  3. css 阻塞 js 解析(執(zhí)行)
    由于 js 可能會讀取或修改 CSSOM,因此需等待CSSOM構(gòu)造完成后,js 才能執(zhí)行??紤]如下代碼:

    <body>
        <h1>hello,</h1>
        <link rel="stylesheet" href="/css/style.css">
        <script src="/main.js"></script>
        <h1>zphhhhh</h1>
    </body>
    

    若 css 加載 2s,則 js 會等待 css 的加載和構(gòu)建 CSSOM,完畢后才會執(zhí)行。當(dāng)然,若上例的 js 聲明了 async / defer,則以 async / defer 的約定執(zhí)行,即可能會 js 不被 css 阻塞。

  4. 擴(kuò)展上述第 2 點(diǎn)即為 js 的解析執(zhí)行幾乎阻塞一切
    不難理解,瀏覽器 js 的單線程模型也使其更加簡單,js 在解析執(zhí)行期間幾乎會阻塞一切,當(dāng)然是指阻塞一切其他資源的解析構(gòu)建渲染等等,不包含加載。

瀏覽器關(guān)鍵渲染路徑

理清了 html / css / js 等資源的加載與阻塞,終于可以開心的繼續(xù)理解瀏覽器的關(guān)鍵渲染路徑了。關(guān)鍵渲染路徑什么鬼?這是指瀏覽器從最初的加載資源到第一次顯示內(nèi)容需要的資源與時(shí)間,考慮下面代碼:

<html>
  <head>
    <link rel="stylesheet" href="style.css">
    <script src="index.js"></script>
    <script src="baidu_#js"></script>
  </head>

  <body>
  </body>
</html>

此時(shí)關(guān)鍵資源數(shù)有 1*.html + 1 * .css + 2 * .js = 4 個(gè),嘗試改為:

<html>
  <head>
    <style>
        /* style.css */
    </style>
    <script>
        // index.js
    </script>
    <script defer src="baidu_#js"></script>
  </head>

  <body>
  </body>
</html>

則關(guān)鍵資源數(shù)只有 1*.html 個(gè),當(dāng)然這只是一個(gè)最簡單的模型,實(shí)際操作當(dāng)然不能把所有 js css 寫到 html 中,意會就好~

優(yōu)化的角度來看,可以從下面幾個(gè)方面考慮:

  1. 考慮到瀏覽器對同一域名最大并發(fā)連接限制,可以減少 http 請求,合并請求,比如合并 css,打包 js,小文件使用 base64 等,Webpppppack~
  2. 適量的多域名提高并發(fā)數(shù)量,但不能太多,因?yàn)?DNS 解析等也需要花費(fèi)時(shí)間。
  3. 啟用壓縮,壓縮靜態(tài)資源體積。
  4. 啟用緩存。
  5. 以及上面講 “加載與阻塞” 提到的方法~

// 暫時(shí)想到這么多,經(jīng)驗(yàn)不足如本文有錯(cuò)誤之處,望請及時(shí)指出,免誤后人,非常感謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容