1. 瀏覽器渲染原理
請說出: 從用戶在瀏覽器地址輸入網址,到看整個頁面,中間都發生了哪些事情?
- HTTP請求階段
- HTTP響應階段
- 瀏覽器渲染階段
1.1 可能用到的知識
1.1.1 進程 Process、線程 Thread、 棧內存 Stack
-
進程: 就是開的每一個程序: QQ、網易云音樂、Typora、VSCode…
-
線程: 一個做的好的事情.
-
棧內存: 用來提供一個環境,供我們執行代碼
1.1.2 多任務
現代操作系統比如Mac OS X, UNIX, Linux, Windows等,都是支持"多任務"的操作系統
單核CPU執行多任務: 操作系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務二,任務2執行0.01秒,再切換到任務3…由于CPU的執行速度很快,我們感覺就像所有任務都再同時執行一樣
多核CPU執行多任務: 真正的并行執行多任務只能在多核CPU上實現,但是,由于任務數量遠遠多于CPU的核心數量,所以,操作系統也會自動把很多任務輪流調度到每個核心上執行
有些進程不止同時干一件事情,就需要同時運行多個"子任務",我們把進程內的這些子任務稱為線程.
多個線程可以同時執行,多線程的執行方式和多進程一樣的,也是由操作系統在多個線程之間快速切換,讓每個線程都短暫地交替運行,看起來就像同時執行一樣
1.2 瀏覽器渲染原理
- 在服務器上有程序員提前寫的項目代碼.
- 它存放在服務器的磁盤中,標識符為
project
- request請求階段:客戶端在瀏覽器輸入網址的時候,瀏覽器會向服務器端發送請求(DNS解析、TCP的三次握手與四次揮手、HTTPS與HTTP的區別)
- response響應階段 :瀏覽器有個專門的端口監聽這個請求,驗證之后,將項目的源代碼返回給客戶端瀏覽器(HTTP狀態碼、304緩存、HTTP報文)
- 客戶端瀏覽器,拿到response響應的代碼后,專門在內存中開辟一個棧內存,給代碼的執行提供環境;同時分配一個主線程,去:
- 一行一行的解析核執行代碼.
<!-- 棧內存 -->
進棧 ---> <!DOCTYPE html> ---> 執行完出棧
- 遇到 link、script、img、a、video等 標簽,主線程,開新建一個子線程去執行相應的內容.自己繼續向下執行,同時會將該異步任務放到任務隊列中
<!-- 棧內存 -->
進棧 ---> <link href="1.css"> ---> 會將異步任務放到 TaskQueue中
<!-- 任務隊列 -->
任務1: 請求1.css
- 根據上述規則: 同步代碼依次執行,遇到請求資源等異步代碼,會創建一個新的子線程,去執行,并將任務放到任務隊列(TaskQueue)中,然后繼續向下執行…
- 因此主線程相當于是一直在執行同步任務.速度會很快
- 很快,主線程執行到頁面底部
<!-- 棧內存 -->
進棧 ---> </html> ---> 出棧
<!-- 任務隊列 -->
任務1: 請求1.css(未完成)
任務2: 請求2.css(未完成)
...
任務n: 請求...(未完成)
-
當執行完畢
</html>
僅僅只在內存中生產了一個DOM樹(注意,此時的異步任務還沒有執行) -
事件循環(Event Loop):生成DOM樹之后,主線程會在任務隊列(Task Queue)中去尋找已經準備好的異步任務,將其取出到棧內存中執行
<!-- 任務隊列 -->
任務1: 請求1.css(未完成)
任務2: 請求2.css(已完成) ---> 取出 ---> [棧內存]
...
任務n: 請求n(已完成)<!-- 棧內存 -->
進棧 ---> 任務2 --->
-
步驟7的機制就是 事件循環(Event Loop), 當任務隊列中的最后一條css獲取成功且在棧內存中執行完畢之后,會在內存中生產一個
CSSOM
.然后瀏覽器會把 CSSOM樹核DOM樹結合在一起生成一棵Render Tree
-
得到渲染樹(Render Tree)之后,進行回流(Layout): 根據生成的渲染樹,計算它們在設備視口(view port)內的確切位置和大小,這個計算階段稱為回流,之后就是重繪(Painting)
-
Painting(重繪): 根據渲染樹以及回流得到的幾何信息,得到節點的絕對像素.
-
絕對像素被分到一個圖層中,每個圖層又會被加載到GPU中形成渲染紋理,最終渲染到頁面上
[注] : 圖層在GPU中, transform是不會觸發repaint的,使用transform的圖層都會由 獨立的合成進程進行處理
1.2.1 性能優化
- (減少HTTP請求次數和數據量大小) : 資源合并與壓縮 (圖片、樣式、JS文件)
- 圖片懶加載: 第一次回流重繪的時候,不加載圖片,當第一次渲染完成之后,當屏幕滾動到哪里,就加載對應位置的數據
1.2.2 優化點
- request階段: DNS解析、TCP的三次握手與四次揮手、HTTPS與HTTP的區別
- response階段: HTTP狀態碼、304緩存、HTTP報文
1.2.2.1 DNS預解析(Chrome瀏覽器完成)
當用戶將鼠標停留在一個鏈接上,就預示著一個用戶的偏好以及下一步的瀏覽行為.
- 這時,Chrome就可以提前進行DNS Lookup及TCP握手
- 當在地址欄觸發高可能性選項時,同樣會觸發DNS lookup 和TCP預連接
- Chrom會研究,每個人每天可能訪問的網址,并對網頁上的子資源嘗試預解析、預加載以提高用戶體驗
1.3 性能優化
-
DOM的重繪和回流 Repaint & Reflow
-
重繪: 元素樣式的改變 (但寬度、大小、位置等不變)
如 outline,visibility,color,background-color等
- 回流: 元素的大小或者位置發生了變化(當頁面布局和集合信息發生變化的時候),觸發了重新布局,導致渲染樹,重新計算布局和渲染.
如添加或刪除可見的DOM元素;元素的位置發生變化;元素的尺寸發生變化;內容發生變化(比如文本變化或圖片被另一個不同尺寸的圖片所代替);頁面一開始渲染的時候(這個無法避免);因為回流是根據視口的大小來計算元素的位置和大小的,所以瀏覽器的窗口尺寸變化也會引發回流
注意: 回流一定會觸發重繪,而重繪不一定會回流
-
進可能減少 回流和重繪: 因為回流會根據當前窗口的大小計算元素的位置,然后回觸發重繪.根據渲染樹和回流的幾何位置信息,算出元素的絕對像素,進而通知GPU重新渲染.
-
避免DOM的回流: mvvm / mvc / virtual dom / dom diff
1.3.1 分離讀寫操作 (現代的瀏覽器都有渲染隊列的機制)
? offsetTop、 offsetLeft 、 offsetWidth、 offsetHeight、 clientTop、 clientLeft、 clientWidth、clientHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、getComputedStyle、currentStyle
<style>#box {width: 100px;height: 100px;background: red;border: 10px solid green;}
</style>
<body><div id="box"></div><script>let box = document.getElementById('box');box.style.width = '200px';box.style.height = '200px';box.style.margin = '10px';</script>
</body>
- 當代瀏覽器,遇到了上面連續對DOM操作的代碼,會將其先存在一個隊列里面,然后一起進行一次回流.
- 但如果遇到如下
<script>let box = document.getElementById('box');box.style.width = '200px';console.log(box.clientWidth);box.style.height = '200px';box.style.margin = '10px';
</script>
- 以上代碼會回流2次,因為在寫操作中穿插了讀操作,將讀操作寫在最下面可以使回流變成一次
<script>let box = document.getElementById('box');box.style.width = '200px';box.style.height = '200px';box.style.margin = '10px';console.log(box.clientWidth);
</script>
1.3.2樣式集中改變
display.cssText = ‘width:20px; height:20px;’
divclassName = “box”
-
不把樣式分開寫,統一寫
-
使用
cssText
<script>let box = document.getElementById('box');box.style.cssText = 'width:200px;height:200px;margin:10px';
</script>
- 使用添加類
<style>.box{width: 200px;height: 200px;margin: 10px;}
</style>
<script>let box = document.getElementById('box');box.className = 'box'
</script>
1.3.3 緩存布局信息
- 看下面代碼
<script>box.style.width = box.clientWidth + 10 + 'px';box.style.height = box.clientHeight + 10 + 'px'
</script>
-
以上會觸發2次回流,因為在遇到
clientWidth、clientHeight
時,瀏覽器都會清空渲染隊列,實現一次回流 -
改進做法:
<script>let width = box.clientWidth + 10 + 'px';let height = box.clientHeight + 10 + 'px';box.style.width = width;box.style.height = height;
</script>
1.3.4 元素批量修改
- 例如我們經常動態的往ul中添加li屬性, 你可能會寫出如下代碼
<ul id="box"></ul>
<script>let box = document.getElementById('box');for(let i =0; i< 5; i++){let li = document.createElement('li');li.innerHTML = i;box.appendChild(li);}
</script>
-
以上會觸發5次回流,可以將DOM操作,次數太少了看不出效果…我們嘗試觸發50000次回流,打開網頁.你會發現瀏覽器卡頓了一下,然后渲染.
-
嘗試使用字符串拼接的方式來減少回流次數
<ul id="strBox"></ul>
<script>let strBox = document.getElement('strBox');let str = '';for(let i =0; i< 50000; i++){str += `<li>${i}</li>`;}strBox.innerHTML = str;
</script>
- 在創建50000個li的情況下, 使用第一種方式大概花費330ms,而第二種方式只需100ms
1.3.5 硬件加速優化
- CSS3硬件加速
[栗子] : 將盒子向右移動100像素
<script>box.style.left = '100px';
</script>
使用上面技術,會使頁面產生一次回流
但是使用transform
則不會產生回流
<script>box.style.transform = 'translateX(200)'
</script>
-
你可能會好奇,為什么使用CSS3的API,不會產生回流,CSS3硬件加速的工作原理
-
不會引起回流重繪的屬性:
transform \ opacity \ filters
-
硬件加速可能存在的缺點: 過多使用會占用大量內存,性能消耗可能會比較嚴重、有時候會導致字體模糊等…
1.3.6 犧牲平滑度換取速度
- 改變動畫的最小平移單位,例如: 將原本需要1像素移動改為3像素…這樣可以減少DOM的節流和重繪
1.4 小結
- 從用戶在瀏覽器地址輸入網址,到看整個頁面,中間都發生了哪些事情?
- 首先瀏覽器會根據輸入的域名,通過域名服務器得ip地址,
- 然后會檢查本地中是否有請求的資源,并且判斷請求資源是否是最新的,如果是則返回,否則去服務器端得到資源(如果有錯誤,希望指出)
- 得到資源后,瀏覽器會在內存中開辟一個棧內存,同時分配一個主線程去從上到下解析文檔結構,遇到
link、img
等標簽會將其放在任務隊列中,繼續向下執行 - 執行到最底部,此時生成了一個 DOM 樹,然后進入事件循環(Event Loop)
- 事件循環: 瀏覽器從 任務隊列(Task Queue)中取出已經準備好的的任務到棧內存中逐條執行
- 結束后會生成一棵CSSOM樹, 然后主線程會根據 DOM樹和 CSSOM樹生成一棵渲染樹(Render Tree)
- 之后會根據 視口大小(View Port)計算出節點的幾何位置(稱為節流)
- 之后是重繪: 根據渲染樹和 節流算出的幾何位置得到節點的確切位置.
- 完成重繪后,主線程會將每個每個絕對位置加載到GPU中,最終渲染成紋理,呈現在頁面上.
參考
Chrome高性能的秘密:DNS預解析
CSS3硬件加速的工作原理
瀏覽器渲染原理