天天看點

浏覽器的渲染過程以及性能優化

前言:網頁的加載速度是使用者對一個網站的首先的使用者體驗,反正個人認為,如果在網好的情況下,打開一個網頁超過3s,我就不不想再進這個網頁了,除非我特别想進這個網站

首先從問題到根源,再到解決方案

  1. 什麼是程序,什麼是線程?
  2. 為什麼js是單線程的?
  3. js和GUI的執行順序,互相會阻塞嗎?
  4. defer 和 async 是什麼,以及它們的差別
  5. 什麼是CRP,即關鍵渲染路徑(Critical Rendering Path)?如何優化?
  6. 什麼是浏覽器的回流與重繪?
  7. 為什麼js是單線程的

程序(process)和線程(thread)

程序(process)和線程(thread)是作業系統的基本概念。

程序是CPU資源配置設定的最小機關(是能擁有資源和獨立運作的最小機關)

線程是CPU排程的最小機關(是建立再程序基礎之上的一次程式運作機關)

線程又分為單線程(至始至終都隻有一個線程工作)和多線程(可能有多個線程共同工作)

程序是CPU發給作業系統的任務,而線程主要是執行任務,一個任務由至少由一個線程,或者多個線程協作完成

由于每個程序至少要做一件事,是以一個程序至少由一個線程,系統會給每個程序配置設定獨立的記憶體,是以程序有獨立的資源。同一程序内的各個線程之間共享該程序的配置設定空間(包括代碼段,資料集,堆)。這也就是為什麼js線程和事件處理線程不是一個線程,但是卻可以操縱相同的資料

當我們啟動一個應用,(或者打開一個浏覽器的标簽頁)計算機會會建立一個程序并且配置設定一塊空間,應用的狀态都會儲存到這塊記憶體中。如果應用關閉程序就會結束,然後還會釋放相應的記憶體

浏覽器的多程序架構

一個好的程序常常被劃分為幾個互相獨立又彼此配合的子產品,浏覽器也是如此。

以Chrome為例,他由多個程序組成,每個程序都有自己的核心的職責,他們互相配合完成浏覽器的整體功能

每個程序中又有多個線程(一個程序最少有一個線程)也會協同工作,配合完成所在的程序的任務的工作

Chrome 采用對程序的架構,其頂層存在一個Browser process 用于協調浏覽器的其他程序工作

多程序的優點

以Chrome 為例,每打開一個标簽頁都是開啟一個新的程序,單個标簽頁崩潰并不會引起浏覽器的奔潰,除非單個程序所占用的記憶體太高,會引起程式的崩潰。

多程序的缺點

由于每開啟一個程序都會占用的一定的記憶體和CPU,如果程序太多額你存占用太多,也會引起程式的崩潰。Chrome浏覽器自帶記憶體回收機制很良性。

浏覽器的主要程序和職責

主要程序
  • 主程序
  • 第三方插件程序(浏覽器的第三方插件)
  • GPU程序(圖像處理)
  • 渲染程序(本文主要描述程序)等

主程序 Browser process

負責浏覽器界面的顯示與互動。各個頁面的(标簽頁)的管理、建立和銷毀其他程序。網絡資源的管理、下載下傳等

第三方插件程序 Plugin process

每種類型的插件對應一個程序,僅當使用該插件時才建立。

GPU 程序 GPU process

最多隻有一個,用于3D繪制

渲染程序 Renderer process

稱為浏覽器渲染程序或浏覽器核心,内部時多線程的。隻要負責頁面渲染(GUI線程),腳本執行(js線程),事件處理(事件處理程序)等

渲染程序的主要線程

GUI渲染線程

負責建構DOM樹和CSSOM樹,然後合并DOM樹和CSSOM樹成 渲染樹,然後進行頁面的布局渲染等。當頁面發生重排或者回流的時候,該線程就會重新執行

注意GUI線程和js線程正常情況下時互斥的

一般情況下等GUI線程執行完成以後才執行js線程,試想一下,js程序主要是圍繞着DOM寫互動,如果先執行js線程,js擷取某個DOM的時候,這個DOM在浏覽器中中還沒有

js引擎線程

JavaScript引擎(v8引擎),也稱為js核心,負責處理js腳本程式

js引擎一直等着任務隊列的到來,然後加以處理,一個渲染程序無論什麼時候都隻有一個js線程運作js,js線程和GUI線程互斥

事件觸發線程

歸屬于浏覽器核心(渲染程序)而不是js引擎,用來控制異步隊列事件循環(可以了解為這是一個輔助性的線程,輔助js引擎),另外輔助線程還有異步http線程,定時器線程等,這些輔助線程隻是幫助處理,最後還是得js執行(js空閑得情況下)

定時器觸發線程

setInterval、setTimeout

浏覽器定時器并不是由js記數的(因為js引擎是單線程的,如果定時器處于阻塞程序,會影響記數的準确性)而是通過單獨的線程計時觸發(計時完畢後添加到事件隊列中等待js引擎孔憲後執行)

異步的http請求線程

在 XMLHttpRequest 在連接配接後是通過浏覽器新開的一個線程請求,将檢測到狀态變更時,如果設定後回調函數,異步線程就産生狀态變更事件,将這個回調放入事件隊列中,再由js引擎執行

浏覽器渲染的流程以及個線程的工作

  1. 渲染程序簡析字元串
  2. 碰到html檔案建構DOM樹
  3. 碰到link css建構CSSDOM樹(碰到script标簽先檢查有沒有async或者defer屬性(有的情況下這個後面說)如果沒有,行進入隊列,在整個GUI線程沒有執行完畢不會執行js線程,除非将script标簽放在前面)
  4. 将兩顆樹合并成renderer樹
  5. GUI線程完成頁面的繪制
  6. 開始執行隊列中的js,開啟js線程
  7. js同步執行代碼
  8. 碰到定時器,定時器線程執行,将處理好的回調放入事件隊列中等待js空閑的時候執行
  9. 碰到異步請求或者異步回調,異步線程和事件觸發線程執行,将改狀态的回調放入事件隊列中(等待js空閑的時候執行)
  10. 當js空閑下來的時候(也就是同步任務執行完畢的時候)開始執行事件隊列中的任務
  11. 而在執行任務的時侯,任務又分為宏任務和微任務,進入這樣一個事件循環

為什麼js線程和GUI線程隻能執行一個

如果同時執行的情況下,試想一個場面,GUI正在建構DOM樹或者CSSDOM樹,如果這時候js線程操縱一個DOM并改變了樣式,很容易發生不可估量的錯誤(抛開在js線程執行的時候,這個DOM存不存在的問題),反之亦然。

css加載會阻塞html加載嗎

會,在建構tree時,編譯時兩個程序,并且時并發的,兩個互相不影響,但是别忘了,單靠一個DOM或者一個CSSDOM樹還不夠,别忘了,GUI線程最終會将這兩個樹給結合,隻要有一顆樹沒有渲染完成,就一直等着

js為什麼是單線程的

其實這個和js的作用還是有關系的,js組要作用是給浏覽器添加一些互動效果,操作DOM,試想一下,如果一個線程在建立一個DOM,而另一個線程删除這個DOM,兩個線程并發執行,可以想想後果

什麼是關鍵渲染路徑(CRP)

關鍵渲染路徑是浏覽器将HTML CSS js轉化為螢幕上呈現的像素内容所經曆的一系列步驟。也就是合并兩棵樹之後的渲染流程

為盡快完成首次渲染,我們需要最大限度減小以下三種可變因素:

  • 關鍵資源的數量: 可能阻止網頁首次渲染的資源。
  • 關鍵路徑長度: 擷取所有關鍵資源所需的往返次數或總時間。
  • 關鍵位元組: 實作網頁首次渲染所需的總位元組數,等同于所有關鍵資源傳送檔案大小的總和。

優化方案

  • 删除不必要的代碼和注釋包括空格,盡量做到最小化檔案
  • 利用gzip壓縮檔案
  • 靜态檔案的緩存
  • 減少css選擇器的過多嵌套

優化js加載方案

上文也提到,由于js線程和GUI渲染線程彼此會互相阻塞,優化方案就在script的兩個屬性上

  • async :當渲染程序碰到script标簽,并且檢查到有async屬性的情況下,兩個線程将不會阻塞,會同時并發

需要注意的情況:js沒有操作DOM的情況下

  • defer :當渲染程序碰到script标簽,并且檢查到有defer屬性的情況下,兩個線程将不會阻塞,但js線程隻會加載js而并不會執行js
js