現代瀏覽器內部工作原理
彙總圖
計算機的核心CPU和GPU
CPU:Central Processing Unit(中央處理器),中心處理器是計算機的大腦,每個CPU核心會逐一執行不同任務現在很多計算機都是多晶片,多核心的。
GPU:Graphics Processing Unit(圖形處理器),GPU擅長處理跨核心的簡單任務,是為了解決圖形而開發的。在圖形環境中,“使用GPU支援”和“使用CPU”都與快速渲染與順滑渲染有關。
許多帶扳手的GPU核心說明它們只能執行處理有限任務
當啟動電腦時候,是CPU和GPU為應用供能。通常情況下,應用是通過作業系統提供的機制在CPU和GPU上執行。
三層計算機繫結構:底部是機器硬體,中間是作業系統,頂層是應用程式。
在程式和執行緒上執行程式 程式(process)和執行緒(thread)
程式作為邊界框,執行緒為裡面抽象的魚在遊動
程式可以被描述為一個應用的執行程式,執行緒存在於程式並執行任意部分。
啟動應用時,會建立一個或者多個程式來幫助應用工作,作業系統為程式提供了一塊可以使用的“記憶體”,應用的所有狀態都儲存在該私有記憶體空間中,關閉應用,程式會關閉,作業系統釋放記憶體。
程式可以請求作業系統的另一個程式來執行不同的任務。此時,會分配不同的記憶體給新程式,程式之間可以通過(IPC:Inter Process Communication)進行通訊。應用的某個工作程式失去響應,該程式就可以在不停止應用程式其他程式的情況下,重啟。
獨立程式通過IPC通訊示意圖
瀏覽器架構
不同的瀏覽器可能是多程式或者單程式的。
Chrome瀏覽器架構,使用多程式的架構
瀏覽器頂層是瀏覽器程式(Browser process),與瀏覽器其他應用模組進行協調工作。
Chrome瀏覽器各個程式的工作:
- Browser Process:
- 位址列,書籤欄,前進後退按鈕等資訊
- 不可見的一些操作,請求,檔案訪問等
- Rederer Process
- 負責一個網頁tab的的所有工作
- Plugin Process
- 外掛程式,處理一個網頁用到的外掛,例如Flash的外掛
- GPU Process
- 負責處理GPU相關的工作 *還有其他程式,例如:擴充套件應用,應用程式
不同程式指向瀏覽器 UI 的不同部分
谷歌瀏覽器每一個站點(網頁)都是獨立的程式,這樣可以保證即使網頁崩潰了,其他網頁也不會受到影響。瀏覽器有多個程式的好處就是安全性跟跟沙箱化。由於作業系統提供了限制程式許可權的方法,瀏覽器就可以用沙箱保護某些程式。由於程式有自己的私有記憶體空間,所以每個程式都有公共基礎設施的拷貝,這也意味著會佔用更多的記憶體,當執行達到極限時候,Chrome對於同一站點的不同標籤頁,會使用同一程式。
當Chrome執行在強力硬體上時,會把不同的服務功能模組分配到不同的程式,從而提升穩定性,但是當執行在弱硬體裝置時候,會將一些服務功能模組整合到同一個程式以節約記憶體,但是相應的穩定性也會下降。
每個Iframe的渲染程式)——站點隔離
站點隔離實現每個Iframe執行在獨立的渲染程式。每個tab站點單獨執行一個程式,站點內的Iframe執行一個單獨的渲染程式,這樣在不同的站點內,相同Iframe可以共享記憶體。
同源策略是web的安全模型,也就是某一站點在沒有授權的情況下,其他站點是不能獲取其資料的。程式隔離是分離站點的最高效手段。
站點隔離示意圖
導航時候發生了什麼?
它以瀏覽器程式(Browser Process)開始
瀏覽器程式管理一切除Ta外之外的一切,瀏覽器程式內有很多執行緒,例如繪製瀏覽器的按鈕和輸入欄的UI執行緒、處理網路棧以從因特網獲取獲取的網路執行緒、控制檔案訪問的儲存執行緒。當輸入URL,輸入由瀏覽器的程式的UI執行緒處理。
頂部是瀏覽器 UI,底部是擁有 UI、網路和儲存執行緒的瀏覽器程式圖
一個簡單的導航
- 處理輸入
瀏覽器程式的UI執行緒首先確定是搜尋查詢還是一個URL。UI執行緒決定是搜尋內容到搜尋引擎還是去一個網站。
UI 執行緒詢問輸入內容是搜尋查詢還是 URL 地址
- 當按下Enter鍵,UI執行緒啟用網路去調取獲取站點內容,載入動畫會顯示在標籤頁的一角。網路執行緒會通過適當的協議,為請求建立TLS連線。
SSL(Secure Sockets Layer 安全套接層),及其繼任者傳輸層安全(Transport Layer Security,TLS)是為網路通訊提供安全及資料完整性的一種安全協議。TLS與SSL在傳輸層對網路連線進行加密。
SSL協議位於TCP/IP協議與各種應用層協議之間,為資料通訊提供安全支援。SSL協議可分為兩層: SSL記錄協議(SSL Record Protocol):它建立在可靠的傳輸協議(如TCP)之上,為高層協議提供資料封裝、壓縮、加密等基本功能的支援。
安全傳輸層協議(TLS)用於在兩個通訊應用程式之間提供保密性和資料完整性。該協議由兩層組成: TLS 記錄協議(TLS Record)和 TLS 握手協議(TLS Handshake)。較低的層為 TLS 記錄協議,位於某個可靠的傳輸協議(例如 TCP)上面,與具體的應用無關,所以,一般把TLS協議歸為傳輸層安全協議。
Roger[ˈrɑ:dʒə(r)]:無線通訊答語:收到。UI 執行緒告訴網路執行緒要導航到 mysite.com
在這時,網路執行緒可能會收到像 HTTP 301 那樣的伺服器重定向頭。這種情況下,網路執行緒會告訴 UI 執行緒,伺服器正在請求重定向。然後,另一個 URL 請求會被啟動。
- 讀取響應
包含 Content-Type 的響應頭以及作為實際資料的 payload
一旦開始收到響應主體,網路執行緒會檢視資料流的前幾個位元組,響應報文的Content-Type欄位會宣告資料的型別。但可能存在丟失會錯誤。所以有了MIME型別嗅探來解決這個問題。
如果響應是一個HTML檔案,那麼下一步就會把資料傳給渲染程式,如果是一個下載的檔案,意味著是一個下載請求,會把它傳遞給下載器管理器。
網路執行緒詢問一個響應資料是否是從安全網站來的 HTML
此時也會進行safeBrowsing檢查,如果域名和資料匹配到一個惡意網站,那麼網路執行緒會顯示一個警告頁面。此外也會進行Cross Origin Read Bloking (CORB)檢查,以確保敏感的跨域資料不被傳給渲染程式。
- 查詢渲染程式
一旦所有的檢查處理完畢並且網路執行緒確定會導航到請求的站點,網路請求執行緒會告訴UI執行緒所有的資料請求完畢。UI執行緒會尋找渲染程式開始渲染Web頁面。
網路執行緒告訴 UI 執行緒去查詢渲染程式
由於網路請求執行緒會花費時間,一次可以應用一個優化措施,當UI執行緒正傳送一個URL請求給網路執行緒時候,可以同時查詢開啟一個渲染程式待命,如果導航重定向,這個渲染程式或許不會用到。
- 提交導航
現在資料和渲染程式就緒,瀏覽器會傳送一個IPC(程式間通訊)到渲染程式去提交導航,他也會傳遞資料流,所以渲染程式會保持接受HTML資料,一旦瀏覽器程式收到渲染程式已經提交的確認資訊,導航完畢並且文件載入解析開始。
這時,位址列已經更新,安全指示器和站點設定UI會放映新頁面的站點資訊,此標籤頁的session歷史記錄被更新,所以前進後退按鈕會走向剛導航過的站點, 當你關閉標籤頁或者視窗,為了優化Tab/session的還原,session歷史被儲存在硬碟上。
瀏覽器和渲染程式間的 IPC,請求渲染頁面。
額外的步驟:初始載入完畢
一旦導航別提交,渲染程式開始載入資源和渲染頁面,一旦渲染程式渲染完畢,會傳送IPC返回給瀏覽器程式,(這也會所有frameh和onload事件已經觸發和執行完畢後發生)。這時,UI執行緒停止標籤頁上的載入動畫。
導航到另一個站點
簡單導航已經完成,在導航欄在輸入一個URL時,瀏覽器程式會先檢查已經渲染的站點是否關心beforeUnload事件,確認沒有的話,就會執行相同的操作,導航到另一個站點。
befounload事件會在使用者離開或者關閉標籤頁面時候給予提醒“離開此站點”。
注意:不要新增無條件的beforeunload處理程式,會產生延時。
瀏覽器程式向渲染程式傳送 IPC 告訴它將要導航到另一個站點
如果渲染程式啟動了導航(window.location.herf=xxx),渲染程式會先檢查befoeload事件處理程式,會像瀏覽器處理導航一樣執行相同的步驟,唯一不同的是導航請求是由渲染程式傳送到瀏覽器程式的。
當新導航到新站點時,會呼叫一個獨立的渲染程式來處理導航,同時保留當前的渲染程式來處理unload事件。
2 個 IPC(從瀏覽器程式到新渲染程式)告知渲染頁面並告知舊渲染程式解除安裝
Service Worker待補充
渲染程式的內部機制
渲染程式處理網站內容
渲染程式負責標籤內傳送的所有事情。渲染程式中,主執行緒處理伺服器返回給使用者的大部分資料,如果使用了web sworker或Service Sorker,部分JS將由工作執行緒處理。合成和光柵執行緒也在渲染程式內執行,以高效流暢的呈現頁面。
渲染程式的核心工作是將HTML,CSS和JavaScript轉換為使用者可以與之互動的網頁。
渲染程式內部包含主執行緒、工作執行緒、合成執行緒和光柵執行緒
- 解析(Parsing)
- Dom的構建 當渲染程式收到HTML資料時,主執行緒解析文字字串(HTML)並將其轉換為(DOM). DOM是頁面在瀏覽器的內部表現,也是開發人員通過JS與之互動的資料結構
HTML的標籤的解析由HTML Standar決定,HTML規範可以很優雅的處理一些標籤錯誤。
- 子資源載入
網站的影像,CSS,JS外部資源,需要從網路或者快取載入。解析構建DOM時,主執行緒會按處理順序逐個載入,為了加快速度,“預載入掃描器(preload scanner)”會同時進行。如果文件有<img><link>
,預載入掃描器會在瀏覽器程式中傳送請求。
主執行緒解析 HTML 並構建 DOM 樹
3.JS阻塞解析
當解析HTML時,遇到<script>
時,會停止解析接下來的內容,載入js,並執行裡面的程式碼。
- 提示瀏覽器如何載入資源
可以在<script>
標籤加async或者defer屬性,實現非同步載入JS,或者載入JS模組,可以使用 <link rel="preload">
告知瀏覽器當前導航肯定需要該資源,並且你希望儘快下載。
- 樣式計算
只有DOM元素無法確定頁面的外觀,需要結合CSS樣式。主執行緒解析CSS並且確定每個DOM節點計算後的樣式。
主執行緒解析 CSS 以新增計算後樣式
- 佈局樹(Layout Tree)
知道每個節點已經對應的元素樣式,不足以渲染頁面。佈局是計算幾何元素形狀的過程,主執行緒遍歷DOM,計算樣式並建立佈局樹,其中包含xy座標和邊框大小等資訊,佈局樹可能與DOM樹結構樹類似,但它僅包含頁面可見內容的資訊。如果一個元素應用了 display:none,那麼該元素不是佈局樹的一部分。類似地,如果應用瞭如 p::before{content:"Hi!"} 的偽類,則即使它不在 DOM 中,也包含於佈局樹中。
主執行緒遍歷計算樣式後的 DOM 樹,以此生成佈局樹
- 繪製
有了DOM、樣式、佈局樹還不能畫出頁面,還不知道繪製的順序。 例如,有的元素有了z-index,簡單從上到下繪製就會出現圖層高低錯誤。
因為沒有考慮z-index,頁面元素按HTML標記的順序出現,導致錯誤的渲染影像
在繪製步驟,主執行緒遍歷佈局樹(Layout Tree)建立繪製記錄(Paint Records),就像是背景優先,然後矩形,文字。
執行緒遍歷佈局樹並生成繪製記錄
- 更新渲染管道的成本很高
DOM + Style、佈局和繪製樹的生成順序
渲染管道最重要的事情:每個步驟,都是前一個操作的結果用於後一個操作的資料。如果為元素設定動畫,每一幀都要進行相同的處理操作,大多數瀏覽器的1幀/秒 如果瀏覽器丟失了中間部分幀,就會讓人覺得卡頓。
時間軸上的動畫幀
- 真正繪製一個頁面
- 光柵化
簡單光柵處理示意圖
現在知道了文件的結構,CSS,佈局樹,繪製順序。就是將資料轉化為物理裝置上的畫素了。這個過程成為光柵化。
- 合成
合成處理是將頁面的各個部分光柵化,並且合成執行緒進行圖層移動合成。
- 分層
為了分清哪些元素位於什麼圖層,主執行緒遍歷佈局樹建立圖層樹,如果某些部分是單獨圖層(例如劃入式側面選單欄),但沒有拆分出來,可以用CSS屬性:will-change提示瀏覽器。
- 主執行緒的光柵化和合成
一旦建立了圖層樹和確定了繪製順序,主執行緒將會把資訊傳遞給合成執行緒,接著,合成執行緒會光柵化每個圖層,一個圖層可能跟頁面一樣大,合成執行緒將其分塊後傳送給光柵執行緒。光柵執行緒光柵化每個小塊後將他們儲存在視訊記憶體中。
光柵執行緒建立分塊的點陣圖併傳送到 GPU
合成執行緒會不同的光柵化執行緒設定優先順序,以便檢視或者附近區域的畫面可以先光柵化顯示。圖層還具有不同的解析度的塊,可以放大顯示。
一旦塊被光柵化,合成執行緒會收集這些塊的資訊(稱為繪製四邊形),建立合成幀。
- 繪製四邊形:包含塊在記憶體的位置,以及合成時塊在頁面中的位置等資訊。
- 合成幀:一個繪製四邊形的集合,代表一個頁面的一幀。
接著,合成幀通過IPC提交給瀏覽器程式,此時,可以在UI執行緒或者其他外掛的渲染程式新增一個合成幀,這些合成器幀被送到GPU然後在螢幕上顯示。如果收到滾動事件,合成幀會建立另一個合成幀到GPU。
合成執行緒建立合成幀,將其傳送到瀏覽器程式,再接著傳送到 GPU
使用者輸入行為與合成器
使用者的任何行為,對於瀏覽器來說都是輸入行為。包括點選,滾動,觸控螢幕,滑動滑鼠。
例如使用者觸控螢幕時,瀏覽器程式率先捕捉行為,瀏覽器程式所掌握的資訊僅限於行為發生的區域,因為標籤內的內容都由渲染程式處理。瀏覽器程式會將點選行為以及座標傳達給渲染程式。渲染程式在進行相應的處理。
合成器接收輸入事件
懸於頁面圖層的檢視視窗
理解非立即可滾動區
執行JS是渲染程式的主執行緒的工作,頁面合成之後,註冊了事件的區域叫做“非立即可滾動區”,合成器執行緒會通知渲染程式的主執行緒處理。沒有輸入事件沒有發生在事件註冊區域,合成器程式則不需要等待主執行緒,可以繼續合成幀。
設定時間處理應該注意
document.body.addEventListener('touchstart',
event => {
if (event.target === area) {
event.preventDefault();
}
});
複製程式碼
事件代理是瀏覽器常用的事件處理模式,在頂層元素新增一個事件。 這樣帶來的問題就是整個頁面都被標識為非立即滾動區域,合成器程式需要每次都詢問主執行緒是否需要處理事件並且等待反饋。流暢的合成器處理模式就失效了。
你可以給事件監聽新增一個 passive:true 選項 ,將這種負面效果最小化。這會提示瀏覽器你想繼續在主執行緒中監聽事件,但合成器不必停滯等候,可接著建立新的合成幀。
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true});
複製程式碼
不過上述寫法可能又會帶來另外一個問題,假設某個區域你只想要水平滾動,使用 passive: true
可以實現平滑滾動,但是垂直方向的滾動可能會先於event.preventDefault()
發生,此時可以通過 event.cancelable
來防止這種情況。
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // 阻止預設的滾動行為
/*
* 這裡設定程式執行任務
*/
}
}, {passive:: true});
複製程式碼
也可以使用css屬性 touch-action
來完全消除事件處理器的影響,如:#area{touch-action:pan-x;}
查詢事件物件
當組合器執行緒傳送輸入事件給主執行緒時,主執行緒首先會進行命中測試(hit test),來查詢對應的時間目標,命中測試會基於渲染過程中生成的繪製記錄(paint records)查詢事件發生座標下尋找的元素。
主執行緒檢查繪製記錄查詢座標 x、y 處繪製內容
#####事件的優化
一般我們螢幕的重新整理速率為 60fps,但是某些事件的觸發量會不止這個值,出於優化的目的,Chrome 會合並連續的事件(如 wheel, mousewheel, mousemove, pointermove, touchmove ),並延遲到下一幀渲染時候執行 。而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非連續性事件則會立即被觸發。
使用 getCoalescedEvents 獲取幀內事件
事件合併可幫助大多數 web 應用構建良好的使用者體驗。然而,如果你開發的是一個繪圖類應用,需要基於 touchmove 事件的座標繪製線路,那麼在你試圖畫下一根光滑的線條時,區間內的一些座標點也可能會因事件合併而丟失。這時,你可以使用目標事件的 getCoalescedEvents 方法獲取事件合併後的資訊。
window.addEventListener('pointermove', event => {
const events = event.getCoalescedEvents();
for (let event of events) {
const x = event.pageX;
const y = event.pageY;
// 使用 x、y 座標畫線
}
});
複製程式碼
參考: