# Performace Day 08-09
## Day08 X 瀏覽器架構演進史 & 渲染機制
1. 第一部分會透過**簡易系統架構**的角度去看瀏覽器的演進,下列是三個必備的知識點:
1. Program 程式:是工程師撰寫的程式碼的集合,例如 Line、 Chrome 就個別是 program。
- 而他們的特點是**還沒有被執行**,因此也就還沒有被載入至記憶體中,而是存放在[次級儲存裝置](https://zh.wikipedia.org/wiki/%E6%AC%A1%E7%B4%9A%E5%84%B2%E5%AD%98%E8%A3%9D%E7%BD%AE)(例如:硬碟)中。
2. Process 程序(對岸用語為進程):**指被執行且載入記憶體的 program。**
- Process 也是 OS **分配資源的最小單位**,可以從 OS 得到如 CPU Time、Memory…等資源,意思是這個 process 在運行時會消耗多少 CPU 與記憶體。
3. Thread 執行緒(對岸用語為線程):**存在於 process 裡面,而一個進程裡至少會有一個線程**。
- 前面有說 process 是 OS 分配資源的最小單位,而 thread 則是作業系統能夠**進行運算排程的最小單位**,也就是說實際執行任務的並不是進程,而是進程中的線程。
- 一個進程有可能有多個線程,其中多個線程可以共用進程的系統資源。
- 可以把進程比喻為一個工廠,線程則是工廠裡面的工人,負責任務的實際執行。

(還不太了解兩者概念的讀者可以參考[關於 Process 與 thread 的文章](https://oldmo860617.medium.com/%E9%80%B2%E7%A8%8B-%E7%B7%9A%E7%A8%8B-%E5%8D%94%E7%A8%8B-%E5%82%BB%E5%82%BB%E5%88%86%E5%BE%97%E6%B8%85%E6%A5%9A-a09b95bd68dd))
2. 第二部分則會介紹瀏覽器的**渲染引擎運作機制**與**渲染流程**,是我們想**提升網頁效能**一個非常重要的切入點。
### 第一部分:瀏覽器架構演進史
#### 1. Single Process 瀏覽器時期
在 2007 年以前,瀏覽器的所有功能模組都是運行在同一個 Process 裡的。

由上面的架構圖也可以看出不同的功能模組可能運行在不同的 thread 中,然而這種架構衍伸出了幾個明顯的問題。
1. **不穩定性**:單一程序的瀏覽器中,其中一個功能模組(例如插件)如果出問題壞掉了,會導致整個瀏覽器的崩潰。
2. **不流暢性**:
- 由於是單一執行緒,意味著同一時刻只有一個功能可以執行。
- 如果遇到一段跑很慢或是無限循環的 JS 程式碼,這段程式碼會獨佔整個 thread 的資源,導致其他模組永遠沒有機會被執行到,這樣的狀況會讓整個瀏覽器失去反應或是變得卡頓。
- 記憶體的空間也是影響瀏覽器效能的重大因素,Single Process 架構的瀏覽器不能完全地回收記憶體,**記憶體佔用**隨著使用時間越久變得**越來越高**。
3. **安全性問題**:沒有實作合理的安全環境(例如 sandbox),因此透過 Plugins 或是 Script 有可能可以獲取系統資源與權限,衍伸出安全性的問題。
#### 2. Multi Processes 瀏覽器時期

- 與 Single Process 的架構差別在於從 Browser Process 中獨立出了兩個 Process:
1. Renderer Process 渲染程序:主要負責頁面的渲染
2. Process 插件程序:專門運行瀏覽器插件、外掛的程序。
不同 Processes 間需要通過 IPC 來溝通(也就是上圖的虛線)。
> IPC:Inter-Process Communication(行程間通訊),指至少兩個行程或執行緒間傳送資料或訊號的一些技術或方法。
- 獨立出 Process 的好處(並解決Single Process 架構的問題):
1. ✅ 解決不穩定性的問題:因為各個 Process 是互相隔離的,也就是說如果 Plugin 崩壞,只會影響到它們當前所運行的 Process,並不會導致整個瀏覽器的癱瘓,因此有效解決了不穩定性的問題。
2. ✅ 解決不流暢性的問題:瀏覽器中每一個 Tab 都會運行在獨立的 Renderer Process 上。
- 解決「同一時刻只有一個功能可以執行」:即使遇到上面無限循環的 Script,仍會造成頁面失去反應,不過會影響的就只有當前的 Tab 而已,其他的 Tab 因為是運行在不同的 Process,因此仍能正常運作。
- 解決「記憶體佔用」:儘管現在頁面失去反應了,當關閉它時,整個 Renderer Process 也會被關閉,這個 Process 所佔用的記憶體會被系統完整的收回,解決了過往記憶體佔用的問題。
3. ✅ 解決不安全性的問題:引用了 「Sandbox 沙盒」的機制,使 Plugin Process 與 Renderer Process 運行在沙盒中。
#### 3. 現代:更加豐富的 Multi Processes 瀏覽器架構
- Chrome 瀏覽器仍然以多程序架構為基礎,不過獨立出了更多的 Processes。

- 更多的 Processes:
1. Network Process:主要負責網路資源載入
2. GPU Process:負責一些頁面的繪製與運算
- **獨立出更多 Processes 的缺點?**
- ✅ 獨立出 Process 的好處:
1. 解決不穩定性、不安全性與不流暢性
2. 透過 process parallel 運行帶來的性能提升
- ❌ 獨立出 Process 的缺點:
1. 更高的記憶體佔用
2. 系統架構會變得更加複雜(要考慮不同 process 間的溝通)
這是個非常複雜的問題,也是 Chrome 團隊一直在優化的方向
#### 4. 未來世界:SOA 服務導向架構瀏覽器 (Services Oriented Architecture)
一個彈性的解決方案解決高記憶體佔用與架構複雜的問題。

- 將瀏覽器程式的每個部分作為 Services 運行,從而可以輕鬆地拆分為不同的 process 或聚合為一個 process。
Processes 間透過 IPC 來溝通,讓系統架構實現高內聚、低耦合、易擴展與易維護的特性。
- Chrome 會根據設備運用採用不同運行方式:
- ✅ 性能較**高**的設備:拆成**多個 processes 的架構**去增強**穩定性**。
- ❌ 性能較**低**的設備:採用多個服務**合併成單一 Process** 的方式來**節省記憶體**佔用。
### 第二部分:現今瀏覽器渲染引擎的運作機制
#### 1. **每個 Tab 都會產生一個獨立的 Renderer Process**
如何查看:Chrome 瀏覽器的右上角,點擊「更多工具」->「工作管理員」


- 有些頁面顯示為「子頁框」,並且沒有獨立的 Process ID,這是為什麼呢?(見第 3 點)
#### 2. **Per-frame Renderer Processes — Site Isolation: 同源, Tab, iframe**
[Same Origin Policy 同源政策](https://developer.mozilla.org/zh-TW/docs/Web/Security/Same-origin_policy)是 Web 裡一個很普遍的安全模型,理論上**不同源**的網站在未經授權下是要**不能存取**到彼此的資源的,不然會產生許多安全性問題。
而要做到**把兩個不同來源的網站徹底分開**,**獨立 Process** 成為最有效率也最根本的一個方式。
因此在 Chrome 中,實現了每個 **Tab 都獨立一個程序的機制。**
- 如果在網頁中**嵌入不同來源的 iframe**,該 iframe 也會運行在**不同的 Renderer Process** 上。
#### 3. ****Process Per Site Instance:子頁框, same site****
- 預設狀況下:每個 Tab 都會是獨立一個 Renderer Process。
- 在「Same Site」的狀況下:Chrome 預設會將 Same Site的網頁運行在同一個 Process 中。
- Same Site 指的是 **Protocol ㄧ樣**、**root domain** 一樣就符合了。

```jsx
// 都會被視為 Same Site
https://kylemo.com
https://www.kylemo.com
https://www.kylemo.com:3000
```
- 透過 **`<a>` tag** 或是 **`window.open`** 等方法,從一個頁面打開另一個頁面也會是 Same Site ,因為:
- 有些 Same Site 的網頁有**共享 JavaScript 執行環境**的需求
- ✅ 能**節省記憶體**
#### 4. **Render Process**
1. 取得資源 (HTML)
- 主要負責的就是頁面的渲染流程。
1. 在瀏覽器輸入 URL 並按下 Enter 後
2. **搜尋列會先對輸入做解析**:判斷使用者輸入的是 URL 還是搜尋關鍵字
3. 並透過 Network Thread 或是 Network Process 去做資源請求
4. 並**根據回傳的 Content-Type 來決定下一步要交給誰做**:如果是回傳的是 **HTML**,就會準備交由 Renderer Process 進行渲染流程。
2. 渲染畫面: Layer, Compositing (rasterize, tile)

- 大致上網頁的渲染流程為:
1. 讀取 HTML 後生成 DOM Tree
2. 讀取 HTML 中的 CSS Link Tag 生成 CSSOM Tree
3. DOM Tree 與 CSSOM Tree 共同生成 Render Tree
4. 根據 Render Tree 生成 Layout Tree,負責各元素大小與位置的計算
5. ⭐️ **Layer 分層:實現一些瀏覽器上的複雜效果例如頁面滾動或是三維空間的排序(詳見下方說明)**
6. 最後 Paint 畫面
7. ⭐️ **Compositing**
- Layer 分層:
- 為了方便實現一些瀏覽器上的複雜效果例如**頁面滾動**或是**三維空間的排序(z-index)**,瀏覽器會作下列事情:
1. 瀏覽器會根據 **Layout Tree** 產生 **Layer Tree**。

2. 在 **Renderer Process** 中的 **Main Thread** 產生**繪製指令**:告訴瀏覽器在哪個座標要繪製線或是繪製幾何圖形等簡單指令的集合。
3. 生成繪製指令後,轉交給 **Renderer Process** 中的另一個執行緒 — **Compositor Thread** 來繪製到頁面上。
- Compositing (rasterize, tile)
- 這時瀏覽器已經獲得了渲染頁面所需要的資訊。
- rasterize 柵格化:也就是把上述頁面資訊轉變成 pixels 顯示在螢幕上。
- 步驟:
1. 創建 Layer tree 並確定繪製順序后,main thread 會將該資訊提交到 compositor thread。
2. 然後,compositor thread 柵格化每個 layer。
3. Layer 可能與頁面的整個長度一樣大,因此 compositor thread 將它們劃分為切片(tile),並將每個切片發送到 **raster threads**。
4. **Raster threads** 柵格化每個 tile 並將其存儲在 **GPU 記憶體**中。
5. Tiles 被柵格化之後,c**ompositor thread** 會匯集被稱作「繪製四邊形 (**draw quads**) 」的資訊來產生 **compositor frame**。
6. 藉由 **IPC** 將 compositor frame 送到 **Browser Process。**
7. 最後 compositor Frame 會被送到 **GPU** 去,顯示到**畫面**上。
#### 5. **Reflow & Repaint & Compositing:頁面更新造成的行為、影響頁面效能**
- 頁面更新造成的 Reflow 回流、Repaint 重繪與 Compositing 合成,這是三個與頁面效能高度相關的概念。
- 如果觸發了渲染流程的某個階段,那麼其之後的階段就也會被觸發。
- 不同的改變樣式的方式,是會觸發不同渲染流程的(Reflow, Repaint, Compositing),因此也是效能優化的一個方向。

- Reflow: 指的是瀏覽器為了重新渲染部分或全部的 document 而重新計算 Render Tree 中元素的物理屬性,如位置、大小的過程。觸發條件為改變一些元素的幾何樣式,例如 height、width、margin 或是排列的方式等等。
- Repaint: 將計算結果轉為實際的像素,畫到畫面上。如果只改動元素的顏色、背景圖等不需要重新計算頁面元素 layout 的樣式,就只會從 Repaint 開始觸發,跳過 Reflow 的步驟,最後再到合成階段。
- Compositing: 也就是剛剛提到的合成。
- 頁面更新簡易流程

- 效能優化方法
- (差)改變一些 Layout 相關屬性: width, height, position 的 left 或 top
- 瀏覽器需要重新計算**頁面元素**
- 觸發 Reflow, Repaint, Compositing
- (普)只改變一些「paint only」的屬性: 背景圖片、字體顏色等
- 瀏覽器**不需要重新計算 layout** 的屬性
- 觸發 Repaint, Compositing
- ✅(佳)Compositing Only: CSS transform
- Compositing 的運作**不是在 Main Thread** 進行的:是在 Compositor Thread 與 Raster Thread
- 只觸發 Compositing
- 如何避免多餘的 Reflow 與 Repaint?
- 避免用多個 statement 修改 style,建議使用**新增或移除 class** 的方式。
- 先**一次讀取**完,再**一起修改**
## Day09 X Resource Hint & Non-Blocking Script Tag
### 1. **Critical Render Path 關鍵渲染路徑**
收到 HTML、CSS 和 JavaScript,再對程式碼進行必需的處理,到最後轉變為顯示像素的過程中的步驟。(詳細渲染步驟可以參考 Day 08)
### 2. **Non-Blocking Script:async, defer**
1. **script 載入流程**
當解析 HTML 時遇到 script tag,會立即載入指定的 JavaScript 並在載入後立即執行它,執行完後才會繼續解析 HTML 的工作。

2. ****async, defer script****
- async script:
async 會非同步去請求外部腳本,回應且載入後會停止解析 HTML,並馬上執行腳本內容。

- 如何使用:
- 寫在 script tag 上
- 透過 JavaScript 動態塞入 script tag:
- 預設會是以 async 的方式載入
- 可以透過設定屬性來將非同步載入關閉。
```js
const script = document.createElement('script');
script.src = "/itironman/kylemo.js";
document.body.append(script);
/* 關閉 async */
script.async = false;
```
- 優點:
- 非同步載入。
- 不需要注意 script 執行順序的問題。
- 不會去動到 DOM 結構。
- 缺點:
- 會打斷 HTML 的解析
- 如果有多個 script 則沒辦法保證執行的先後順序。
- 用途:
- 載入第三方函式庫(例如:Google Analytics 等網站分析工具)等不需要動到 DOM 結構的狀況 :這是由於腳本執行時沒辦法確保 DOM 已經全部渲染。
- defer script:
defer 也會非同步請求外部腳本,但是載入的腳本會等待瀏覽器**解析完 HTML 才執行。**

- 實際上的執行時間,會**在 DOMContentLoaded 執行之前**。
- 類似於把 JS 放在頁尾的情況。
- 如何使用:
- 寫在 script tag 上
- 優點:
- 非同步載入。
- **不打斷 HTML 的解析。**
- 確保執行順序:
- 先後順序是依照 script tag 的順序(由上至下)。
- 用途:
- 基本上如果是不是那麼緊急的 script,都可以加上 defer(這是由於非同步載入、不打斷渲染流程及確保執行順序的特色)
- 對於 Critical Render Path 或資源載入方式有興趣的讀者,可以更進一步閱讀 [Google Developer](https://developers.google.com/web/fundamentals/performance/critical-rendering-path) 的文章或 [MDN script tag 的 document](https://developer.mozilla.org/zh-TW/docs/Web/HTML/Element/script)。
### 3. Resource Hint:對資源預先處理以節省時間
- **目的:對資源預先處理以節省時間。**
「**由我們提供效能優化指令給瀏覽器**」,讓瀏覽器依照我們的指令對不久的將來會用到的**資源預先處理**。這裡的處理有可能是**載入資源、**或是**建立連線**,因此在真的要使用到該資源時可以省去不少時間。
- **優缺點:**
- 優點:增加效能
- 缺點:浪費網路資源(預先載入了一堆根本不會用到的資源,對效能只是負擔而已)
- **嚴格的定義:是帶有 rel attribute 的 link tag(5種)**
1. `preload`:嚴格來說 `preload` **不算是 Resource Hint**,更像是強制性的 command(對瀏覽器而言是 **high priority**)
- 取得「當前頁面」的重要資源
- 為什麼「preload 不算是 Resource Hint」:
- 因為它有自己獨立的 [W3C spec](https://www.w3.org/TR/preload/)。是一個 deprecated 的 subresource prefetching feature 的替代版本。

2. `prefetch`
- 不限於當前頁面使用的資源。
- 可以跨越 navigation。
- 例如:你很確定使用者會點擊下一頁,就可以使用 prefetch 預先抓取下一頁的資源,至於什麼時候要下載,則交由瀏覽器自行決定。
3. `preconnect`
- `<link rel="preconnect" href="https://example.com" />`
- 瀏覽器在實際傳輸資源前,會經過這些步驟:
1. 向 DNS 請求解析網域名,拿到 IP 地址 (DNS Resolution)
2. TCP Handshake
3. (HTTPS connection) SSL Negotiation
4. 建立連線完成,等待拿到資料的第一個byte
每一個步驟都需要一個 RTT (Round Trip Time) 的來回時間。
利用 preconnect 提早建立好與特定 domain 之間的連線,省去了一次完整的 (DNS Lookup + TCP Handshake + SSL Negotiation) ,共**三個 Round Trip Time** 的時間。
- 會使用在確定 **10 秒** 內會用到的 domain:瀏覽器只會保持 preconnect 的 connection 10 秒,超過 10 秒都沒有跟連線目標發送請求,瀏覽器會自動關閉連線。
4. `dns-prefetch`
- dns-prefetch= **DNS look up**
- preconnect = **DNS look up** + TCP Handshake + SSL Negotiation (耗費更多 bandwidth)
- Use Cases
- (👍 best practice) Only For Cross-Origin Domains:因為同源的 IP Address 早就被解析過了。
- preconnect Pair With dns-prefetch
```html
<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin>
<link rel="dns-prefetch" href="https://fonts.googleapis.com/">
```
- 因為 preconnet 的瀏覽器支援度比 dns-prefetch 還差,且瀏覽器看到不支援的 hint,會忽略它而不會報錯 (fault-tolerant),在一起使用的狀況下可以確保最小限度先做 DNS Resolution。
5. `prerender`
- prerender 與 prefetch 都是針對非當前頁面的資源載入。
- prerender 不僅僅會下載對應的資源,還會對資源進行**解析**。
解析過程中,如果需要其他的資源,**還會直接下載或執行這些資源**,基本上就是盡可能預先渲染下個頁面。
這樣一來當用戶在從當前頁面跳轉到目標頁面時,瀏覽器可以快速的響應。
- 要非常確定使用者在不久後一定會存取的頁面,不然反而**浪費了更多的 Network Bandwidth**。
第2~5個 Resource Hint 是真的「Hint」,它們**建議**瀏覽器可以先去載入哪些資源或是做哪些事,對瀏覽器而言,這些 hint 的 **priority 是比較低**的,當瀏覽器有 idle time 再去做就好。
- **瀏覽器支援度:**
- 可以參考[這裡](https://caniuse.com/?search=resource%20hints)查看個瀏覽器對各個功能的支援度。
### 4. 本日小結:使用 JavaScript 動態產生這些 Resource Hint 以便維護
- 使用 Resource Hint 也很容易讓 code 變得難以維護
- 如果都單純把 hint 加到 HTML 裡,萬一資源有變動,要改動的話十分麻煩
- 比較合理的方式可能是使用 JavaScript 動態產生這些 Resource Hint,可以把相關的 hint 寫在同一個 file 維護上也變得更加容易
- Resource Hint 都是 [body-ok](https://html.spec.whatwg.org/multipage/links.html#body-ok) 的 link,要放在 HTML 的 body 也是可以 work 的
# References
[今晚,我想來點 Web 前端效能優化大補帖!](https://ithelp.ithome.com.tw/users/20113277/ironman/3877)