這是我參與8月更文挑戰的第4天,活動詳情查看:8月更文挑戰javascript
這篇文章咱們將從高級架構到渲染管道的細節深刻了解 Chrome 瀏覽器。若是您想知道瀏覽器如何將您的代碼轉換爲功能性網站,或者您不肯定爲何建議使用特定技術來提升性能,那麼本文適合您。css
對於前端開發者,知其然知其因此然很重要,因此只有在知己知彼的狀況下才能更好的駕馭瀏覽器,以期瀏覽器給你帶來更高的性能。html
CPU(中央處理器) 和 GPU(圖形處理器) 做爲計算機中最重要的兩個計算單元直接決定了計算性能。前端
CPU 能夠被認爲是您計算機的大腦。與 CPU 不一樣,GPU 擅長處理簡單的任務,但同時跨多個內核。顧名思義,它最初是爲處理圖形而開發的。這就是爲何在圖形上下文中「使用 GPU」或「GPU 支持」與快速渲染和流暢交互相關聯。近年來,隨着 GPU 加速計算,愈來愈多的計算成爲可能單獨使用 GPU。java
當您在計算機或手機上啓動應用程序時,CPU 和 GPU 是驅動應用程序的驅動力。一般,應用程序使用操做系統提供的機制在 CPU 和 GPU 上運行。web
咱們能夠把計算機自下而上分紅三層:底部是機器硬件,中間是操做系統,頂部是應用程序。有了操做系統的存在,上層運行的應用可使用操做系統提供的能力使用硬件資源而不會直接訪問硬件資源。chrome
進程做爲邊界框,線程做爲在進程內遊動的抽象魚api
在深刻研究瀏覽器架構以前要掌握的另外一個概念是進程和線程
。一個進程
能夠被描述爲一個應用程序的執行程序。線程
是存在於進程內部並執行其進程程序的任何部分的線程。跨域
當您啓動應用程序時,就會建立一個進程。程序可能會建立線程來幫助它工做,但這是可選的。操做系統會爲進程分配私有的內存空間以供使用,當關閉程序時,這段私有的內存也會被釋放。其實還有比線程更小的存在就是協程,而協成是運行在線程中更小的單位。async/await 就是基於協程實現的。瀏覽器
進程間通訊(IPC):一個進程可讓操做系統開啓另外一個進程處運行不一樣的任務。當兩個進程須要通訊時,能夠用 IPC(Inter Process Communication)。
多數程序被設計成使用 IPC 來進行進程間的通訊,好處在於當一個進程給另外一個進程發消息而沒有迴應時,並不影響當前的進程繼續工做。
那麼如何使用進程和線程構建 Web 瀏覽器呢?嗯,它多是一個進程有許多不一樣的線程,也多是許多不一樣的進程有幾個線程經過 IPC 進行通訊。
這裏須要注意的重要一點是,這些不一樣的架構是實現細節。沒有關於如何構建 Web 瀏覽器的標準規範。一種瀏覽器的方法可能與另外一種徹底不一樣。
在這裏咱們將使用下圖中描述的 Chrome 最新架構。
頂部是瀏覽器進程與其餘處理應用程序不一樣部分的進程協調。對於渲染器進程,會建立多個進程並將其分配給每一個選項卡。直到最近,Chrome 還在可能的狀況下爲每一個選項卡提供了一個進程;如今它嘗試爲每一個站點提供本身的進程,包括 iframe(請參閱站點隔離)。
下表描述了每一個 Chrome 進程及其控制的內容:
流程及其控制的內容 | |
---|---|
瀏覽器 | 控制應用程序的「chrome」部分,包括地址欄、書籤、後退和前進按鈕。 還處理 Web 瀏覽器的不可見的特權部分,例如網絡請求和文件訪問。 |
渲染器 | 控制顯示網站的選項卡內的任何內容。 |
插件 | 控制網站使用的任何插件,例如 flash。 |
圖形處理器 | 獨立於其餘進程處理 GPU 任務。它被分紅不一樣的進程,由於 GPU 處理來自多個應用程序的請求並將它們繪製在同一個表面上。 |
還有更多的進程,如擴展進程和實用程序進程。若是您想查看 Chrome 中正在運行的進程數,請單擊選項菜單圖標 more_vert在右上角,選擇更多工具,而後選擇任務管理器。這將打開一個窗口,其中包含當前正在運行的進程列表以及它們使用的 CPU/內存量。
Chrome 使用多個渲染器進程。在最簡單的狀況下,您能夠想象每一個選項卡都有本身的渲染器進程。假設您打開了 3 個選項卡,每一個選項卡都由一個獨立的渲染器進程運行。若是一個選項卡變得無響應,那麼您能夠關閉無響應的選項卡並繼續前進,同時保持其餘選項卡的活動。若是全部選項卡都在一個進程上運行,當一個選項卡變得無響應時,全部選項卡都無響應。那隻能重啓瀏覽器了。
將瀏覽器的工做分紅多個進程的另外一個好處是安全性和沙盒。
由於進程有本身的私有內存空間,因此它們一般包含公共基礎設施的副本(好比 V8,它是 Chrome 的 JavaScript 引擎)。這意味着更多的內存使用量,由於若是它們是同一進程內的線程,它們就不能像它們那樣共享。爲了節省內存,Chrome 限制了它能夠啓動的進程數。該限制取決於您設備的內存和 CPU 能力,但當 Chrome 達到限制時,它會開始在一個進程中運行來自同一站點的多個選項卡。
讓咱們看一個簡單的 Web 瀏覽用例:您在瀏覽器中鍵入一個 URL,而後瀏覽器從 Internet 獲取數據並顯示一個頁面。在這裏咱們將重點介紹用戶請求網站和瀏覽器準備呈現頁面的部分 - 也稱爲導航。
當您在地址欄中鍵入 URL 時,您的輸入由瀏覽器進程的 UI 線程處理。
1. 處理輸入
當用戶開始在地址欄中鍵入內容時,UI 線程首先詢問的是「這是搜索查詢仍是 URL?」。在 Chrome 中,地址欄也是一個搜索輸入字段,所以 UI 線程須要解析並決定是將您發送到搜索引擎,仍是發送到您請求的站點。
由於 Chrome 瀏覽器的地址欄既能夠當作地址欄也能夠當作搜索欄
2. 開始訪問
當用戶按下回車鍵時,UI 線程會發起網絡調用以獲取站點內容。瀏覽器頁籤的標題上會出現加載中的圖標,網絡線程經過適當的協議,如 DNS 查找域名並請求服務器創建 TLS 鏈接。
當服務器返回給瀏覽器重定向請求時,網絡線程會通知 UI 線程須要重定向,而後會以新的地址作開始請求資源。
當服務器返回給瀏覽器重定向請求時,網絡線程會通知 UI 線程須要重定向,而後會以新的地址作開始請求資源。
3. 處理響應數據
當網絡線程收到來自服務器的數據時,會試圖從數據中的前面的一些字節中獲得數據的類型(Content-Type
),以試圖瞭解數據的格式。
當返回的數據類型是 HTML 時,會將數據傳遞給渲染進程作進一步的渲染工做。可是若是數據類型是 zip 文件或者其餘文件格式時,會將數據傳遞給下載管理器作進一步的文件預覽或者下載工做。
在開始渲染以前,網絡線程要先檢查數據的安全性,這裏也是瀏覽器保證安全的地方。若是返回的數據來自一些惡意的站點,網絡線程會顯示警告的頁面。同時,Cross Origin Read Blocking(CORB)策略也會確保跨域的敏感數據不會被傳遞給渲染進程。
4. 渲染過程
當全部的檢查結束後,網絡線程確信瀏覽器能夠訪問站點時,網絡線程通知 UI 線程數據已經準備好了。UI 線程會根據當前的站點找到一個渲染進程完成接下來的渲染工做。
在第二步,UI 線程將請求地址傳遞給網絡線程時,UI 線程就已經知道了要訪問的站點。此時 UI 線程就能夠開始查找或啓動一個渲染進程,這個動做與讓網絡線程下載數據是同時的。若是網絡線程按照預期獲取到數據,則渲染進程就已經能夠開始渲染了,這個動做減小了從網絡線程開始請求數據到渲染進程能夠開始渲染頁面的時間。固然,若是出現重定向的請求時,提早初始化的渲染進程可能就不會被使用了,但相比正常訪問站點的場景,重定向每每是少數,在實際工做中,也須要根據特定的場景給出特定的方案,沒必要追求完美的方案。
5. 提交訪問
經歷前面的步驟,數據和渲染進程都已經準備好了。瀏覽器進程會經過 IPC 向渲染進程提交此次訪問,同時也會保證渲染進程能夠經過網絡線程繼續獲取數據。一旦瀏覽器進程收到來自渲染進程的確認完畢的消息,就意味着訪問的過程結束了,文檔渲染的過程就開始了。
這時,地址欄顯示出代表安全的圖標,同時顯示出站點的信息。訪問歷史中也會加入當前的站點信息。爲了能恢復訪問歷史信息,當頁籤或窗口被關閉時,訪問歷史的信息會被存儲在硬盤中。
額外步驟. 加載完畢
當訪問被提交給渲染進程,渲染進程會繼續加載頁面資源而且渲染頁面。當渲染進程"結束"渲染工做,會給瀏覽器進程發送消息,這個消息會在頁面中全部子頁面(frame)結束加載後發出,也就是 onLoad 事件觸發後發送。當收到"結束"消息後,UI 線程會隱藏頁籤標題上的加載狀態圖標,代表頁面加載完畢。
但這裏"結束"並不意味着全部的加載工做都結束了,由於可能還有 JavaScript 在加載額外的資源或者渲染新的視圖。
一次普通的訪問到此就結束了。當咱們輸入另一個地址時,瀏覽器進程會重複上面的過程。可是在開始新的訪問前,會確認當前的站點是否關心beforeunload
事件。
beforeunload
事件能夠提醒用戶是否要訪問新的站點或者關閉頁籤,若是用戶拒絕則新的訪問或關閉會被阻止。
因爲全部的包括渲染、運行 Javascript 的工做都發生在渲染進程中,瀏覽器進程須要在新的訪問開始前與渲染進程確認當前的站點是否關心unload
。
若是一次訪問是從一個渲染進程中發起的,例如用戶點擊一個連接或者運行 JavaScript 代碼location = 'http://newsite.com'
時,渲染進程首先檢查beforeunload
。而後再執行和瀏覽器進程初始化訪問一樣的步驟,只不過區別在於這樣的訪問請求是由渲染進程向瀏覽器進程發起的。
當新的站點請求被建立時,一個獨立的渲染進程將被用於處理這個請求。爲了支持像unload
的事件觸發,老的渲染進程須要保持住當前的狀態。更詳細的生命週期介紹能夠參考Page lifecycle。
Service worker 是一種能夠 web 開發者控制緩存的技術。若是 Service worker 被實現成從本地存儲獲取數據時,那麼本來的請求就不會被瀏覽器發送給服務器了。
值得注意的是,Service worker 中的代碼是運行在渲染進程中的。當訪問開始時,網絡線程會根據域名檢查是否有 Service worker 會處理當前地址的請求,若是有,則 UI 線程會找到對應的渲染進程去執行 Service worker 的代碼,而 Service worker 可讓開發者決定這個請求是從本地存儲仍是從網絡中獲取數據。
若是 Service worker 最終決定要從網絡中獲取數據時,咱們會發現這種跨進程的通訊會形成一些延遲。Navigation Preload是一種能夠在 Service worker 啓動的同時加載資源的優化機制。藉助特殊的請求頭,服務器能夠決定返回什麼樣的內容給瀏覽器。
渲染進程負責全部發生在瀏覽器頁籤中的事情。在一個渲染進程中,主線程負責解析,編譯或運行代碼等工做,當咱們使用 Worker 時,Worker 線程會負責運行一部分代碼。合成線程和光柵線程是也是運行在渲染進程中的,負責更高效和順暢的渲染頁面。
渲染進程最重要的工做就是將 HTML、CSS 和 Javascript 代碼轉換成一個能夠與用戶產生交互的頁面。
renderer.png
下面的章節主要介紹渲染進程如何將從網絡線程中獲取的文本轉化成圖像的過程。
當渲染進程接收到來自瀏覽器進程提交訪問的消息後就開始接受 HTML 數據,主線程開始解析 HTML 文本字符串,而且將其轉化成 Document Object Model(DOM) 。
DOM 是一種瀏覽器內部用於表達頁面結構的數據,同時也爲 Web 開發者提供了操做頁面元素的接口,讓 web 開發者能夠在 Javascript 代碼中獲取和操做頁面中的元素。
將 HTML 文本轉化成 DOM 的標準被HTML Standard定義。咱們會發如今轉化過程當中瀏覽器歷來不會拋出異常,相似關閉標籤的丟失,開始、關閉標籤匹配錯誤等等。這是由於 HTML 標準中定義了要靜默的處理這些錯誤,若是對此感興趣能夠閱讀An introduction to error handling and strange cases in the parser。
一個網站一般還會使用相似圖片,樣式文件和 JavaScript 代碼等額外的資源。這些資源也須要從網絡或緩存中獲取。主線程在轉化 HTML 的過程當中理應挨個加載它們,可是爲了提升效率,預加載掃描(Preload Scanner)與轉換過程會同時運行着。當預加載掃描在分析器分析 HTML 過程當中發現了相似 img 或 link 這樣的標籤時,就會發送請求給瀏覽器進程的網絡線程,而主線程會根據這些額外資源是否會阻塞轉化過程而決定是否等待資源加載完畢。
當 HTML 分析器發現<script>
標籤時,會暫停接下來的 HTML 轉化工做,而後加載、解析而且運行 Javascript 代碼。由於在 Javascript 代碼中可能會使用相似document.write
這樣的 API 去改變 DOM 的結構。這就是爲何 HTML 分析器必須等待 Javascript 代碼運行結束才能繼續分析的緣由。
若是咱們的 Javascript 代碼並不須要改變 DOM,能夠爲<script>
標籤添加async
或defer
屬性,這樣瀏覽器就會異步的加載這些資源而且不會阻塞 HTML 轉化過程。若是 script 標籤是由 JavaScript 代碼建立的,標籤的 async 屬性會默認爲 true。 同時咱們也可使用一些預加載技術,好比<link ref="preload">
來通知瀏覽器這些資源須要越快下載越好。
對於展現一個頁面,光有 DOM 是不夠的,由於咱們還須要樣式來讓頁面變得更美觀。主線程會解析樣式(CSS)並決定每一個 DOM 元素的樣式。這些樣式取決於 CSS 選擇器的範圍,在瀏覽器開發者工具中咱們能夠看到這些信息。
computedstyle.png
即便咱們沒有給 DOM 指定任何的樣式,<h1>
標籤也會比<h2>
標籤顯示的大。這是由於瀏覽器爲不一樣的標籤內置了不一樣的樣式。能夠經過Chromium源代碼獲得這些默認樣式。
完成了樣式計算工做後,渲染進程已經知道了 DOM 的結構和每一個節點的樣式,可是依然不足以渲染一個頁面。
佈局是爲元素指定幾何信息的過程。主線程遍歷 DOM 結構中的元素及其樣式,同時建立出帶有座標和元素尺寸信息的佈局樹(Layout tree)。佈局樹的結構與 DOM 樹的結構十分類似,但只包含將會在頁面中顯示的元素。當一個元素的樣式被設置成 display: none 時,元素就不會出如今佈局樹中,但那些樣式被設置成 visiblility:hidden 的元素會出如今佈局樹中。 類似的,當咱們使用一個包含內容的僞元素(例如p::before { content: 'Hi!' }
)時,元素會出如今佈局樹中即便這個元素不存在於 DOM 樹中,這也是爲何咱們使用 DOM 提供的 API 沒法獲取僞元素的緣由。
描述頁面佈局信息是一項具備挑戰性的工做,即便在只有塊元素的頁面中也必需要考慮字體的大小和在哪裏換行,由於在計算下一個元素的位置時須要知道上一個元素的尺寸和形狀。
CSS 可讓元素浮動、可讓元素在父元素中溢出,能夠改變文字的方向。能夠想象,在佈局這個階段是多麼繁重的工做。在 Chrome 中,有一整個團隊在維護佈局工做,更詳細的信息能夠觀看視頻。
有了 DOM、樣式和佈局仍是沒法完成渲染工做。試想,當咱們試圖複製一張圖畫。咱們知道圖畫中元素的尺寸、形狀和位置,咱們還須要知道繪製這些元素的順序。
在這個階段,主線程遍歷佈局樹並建立繪製記錄,繪製記錄是一系列由繪製步驟組成的流程,例如先繪製背景,而後是文字,而後是形狀。
在渲染過程當中,任何一個步驟中產生的數據變化都會引發後續一系列的的變化。例如,當佈局樹改變時,繪製須要重構頁面中變化的部分。
當一些元素有動畫發生時,瀏覽器須要在每一幀中繪製這些元素。當沒法保證每一幀繪製的連續性時,用戶就會感受到卡頓。
正常狀況下渲染操做能夠與屏幕刷新保持同步,但因爲這些操做運行在主線程中,也就意味這些操做可能被正在運行的 Javascript 代碼所阻塞。
爲了避免影響渲染操做,咱們能夠將 Javascript 操做優化成小塊,而後使用requestAnimationFrame()
,關於如何優化能夠參考Optimize JavaScript Exectuion。當須要大量計算時,也可使用 Worker 來避免阻塞主進程。
如今,瀏覽器已經知道了文檔結構、每個元素的樣式,元素的幾何信息,繪製的順序。將這些信息轉化成屏幕上像素的過程叫作光柵化,光柵化是圖形學的範疇。
傳統的作法是將可視區域的內容進行光柵化。隨着用戶滾動頁面,不斷的光柵化更多的區域。然而對於現代瀏覽器,有着更復雜的的過程,這個過程被稱作合成。
合成是一種將頁面拆分紅多層的技術,合成線程能夠將各個層在不一樣線程中光柵化,再組合成一個頁面。當滾動時,若是層已經被光柵化,則會使用已經存在的層合成新的幀,動畫則能夠經過移動層來實現。
爲了決定層包含哪些元素,主線程須要遍歷佈局樹以找到須要生成的部分。對開發者來講,當某一部分須要用獨立的層渲染,咱們可使用 css 屬性will-change
讓瀏覽器建立層,關於瀏覽器如何生成層的標準可自行查閱。
雖然經過分層能夠優化瀏覽器性能,但並不意味着應該給每一個元素一個層,過多的層反而影響性能,因此在層的劃分上應該具體形況具體分析。
當佈局樹和繪製順序肯定之後,主線程會將這些信息提交給合成線程。合成線程會光柵化各個層。一個層包含的內容多是一個完整的頁面,也多是頁面的部分,因此合成線程將層拆分紅許多塊,並將它們發送給柵格線程。柵格線程光柵化這些塊並將它們存儲在 GPU 緩存中。
合成線程能夠決定柵格線程光柵塊的優先級,這樣能夠保證用戶能看到的部分能夠先被光柵化。一個層也會包含多種塊以支持相似縮放這樣的功能。
當塊被光柵化後,合成線程會使用 draw quads 收集這些信息並建立合成幀(Compositor frame)。
存儲在緩存中,包含相似塊位置這樣的信息,用於描述如何使用塊合成頁面。
用於存儲表現頁面一幀中包含哪些 Draw quads 的集合。
而後一個合成幀被提交給瀏覽器進程。這時若是瀏覽器 UI 有變化,或者插件的 UI 有變化時,另外一個合成幀就會被建立出來。因此每當有交互發生時,合成線程就會建立更多的合成幀而後經過 GPU 將新的部分渲染出來。
合成的好處在於其獨立於主線程。合成線程不須要等待樣式計算和 Javascript 代碼的運行。這也是爲何合成更適合優化交互性能,但若是佈局或者繪製須要從新計算則主線程是必需要參與的。
本質上,瀏覽器的渲染過程就是將文本轉換成圖像的過程,而當用戶與頁面發生交互動做時,則顯示新的圖像。在這個過程當中由渲染進程中的主線程完成計算工做,由合成線程和柵格線程完成圖像的繪製工做。而在計算過程當中,還有強制佈局、重排、重繪等更加細節的概念會在後面的文章中作講解。
當咱們聽到事件時,一般會聯想到在一個文本框中輸入或者單擊鼠標,但從瀏覽器的角度看,輸入事件意味着全部的用戶動做。鼠標滾輪滾動或者屏幕觸摸都是輸入事件。
當用戶與頁面發生交互時,瀏覽器進程首先接收到事件,然而,瀏覽器進程只關心事件發生時是在哪一個頁籤中,因此瀏覽器進程會將事件類型和位置信息等發送給負責當前頁籤的渲染進程,渲染進程會恰當的找到事件發生的元素而且觸發事件監聽器。
在前面的章節中,咱們知道了合成線程能夠經過合成技術合成不一樣的光柵層優化性能,若是頁面並不監放任何事件,合成線程能夠徹底獨立於主線程生成新的合成幀。但若是頁面監聽了事件呢?
因爲運行 Javascript 是主線程的工做,當頁面被合成線程合成過,合成線程會標記那些有事件監聽的區域。有了這些信息,當事件發生在響應的區域時,合成線程就會將事件發送給主線程處理。若是在非事件監聽區域,則渲染進程直接建立新的幀而不關心主線程。
在 web 開發中常見的方式就是事件代理。利用事件冒泡,咱們能夠在目標元素的上層元素中監聽事件。參照下面的代碼。
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault(); }});
複製代碼
經過這種寫法,能夠更高效的監聽事件。但若是從瀏覽器的角度看,此時整個頁面會被標記成「慢滾動」區域。這意味着雖然頁面中的某些部分並不須要事件監聽,但合成線程依然要在每次交互發生後等待主線程處理事件,合成線程的優化效果不復存在。
爲了解決這個問題,咱們可在事件代理時傳入passive: true
(IE 不支持) 參數。這樣告訴渲染線程,依然須要將事件發送給主線程處理,但不須要等待。
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault() } }, {passive: true});
複製代碼
關於使用 passive 改善滾屏性能,能夠參考MDN 使用passive改善滾屏性能。
當渲染線程將事件發送給主線程後,第一件事就是找到事件觸發的目標。經過在渲染過程當中生成的繪製信息,能夠根據座標找到目標元素。
爲了保證動畫的順暢,須要顯示器在每秒刷新 60 次。對於典型的觸摸事件由合成線程提交給主線程的事件頻率能夠達到每秒 60-120 次,對於典型的鼠標事件每秒會發送 100 次。事件發送的頻率一般比屏幕刷新頻率要高。
若是相似touchmove
這樣的事件每秒向主線程發送 120 次可能會形成主線程執行時間過長而影響性能。
爲了減小發送給主線程的事件數量,Chrome 合併了連續的事件。相似wheel
,mousewheel
,mousemove
,pointermove
,touchmove
這樣的事件會被延遲到下一次requestAnimationFrame
前觸發.
而任何的離散事件,相似keydown
, keyup
, mouseup
, mousedown
, touchstart
和 touchend
都會當即被髮送給主線程處理。
到此,咱們已經能夠經過從用戶在瀏覽器地址欄中的一次輸入到頁面圖像的顯示瞭解瀏覽器是如何工做的。這裏咱們總結一下。
最後,感謝你的閱讀。
文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊👍和關注😊,但願點贊多多多多...