翻譯:瘋狂的技術宅 原文:developers.google.com/web/updates…javascript
這是關於瀏覽器內部工做原理系列的第3部分。 以前,咱們介紹了多進程架構和導航流程。 在這篇文章中,咱們將看看渲染器進程內部發生了什麼。java
渲染進程涉及Web性能的諸多方面。 因爲渲染進程中發生了不少事情,所以本文不能一一贅述。 若是你想深刻挖掘,能夠在Web基礎的性能部分找到更多內容。web
渲染器進程負責選項卡內發生的全部事情。 在渲染器進程中,主線程處理你爲用戶編寫的大部分代碼。 若是你使用了web worker 或 a service worker,有時JavaScript代碼的一部分將由工做線程處理。 排版和柵格線程也在渲染器進程內運行,以便高效、流暢地呈現頁面。chrome
渲染器進程的核心工做是將HTML、CSS和JavaScript轉換爲用戶能夠與之交互的網頁。canvas
圖1:渲染器進程內部有主線程、工做線程、排版線程和柵格線程瀏覽器
當渲染器進程收到導航的提交消息並開始接收HTML數據時,主線程開始解析文本字符串(HTML)並將其轉換爲文檔對象模型(DOM—Document Object Model )。緩存
DOM是頁面在瀏覽器中的內部表示,同時也是Web開發人員能夠經過 JavaScript 與之交互的數據結構和API。網絡
HTML標準將HTML文檔解析爲DOM。 你可能已經注意到,將HTML提供給瀏覽器從不會引起錯誤。 例如,缺乏結束</ p>標記是有效的HTML。 像 Hi! <b>I'm <i>Chrome</b>!</i>
這樣的錯誤標記(b標籤在i標籤以前被關閉)被看做是 Hi! <b>I'm <i>Chrome</i></b><i>!</i>
。 這是由於HTML規範旨在優雅地處理這些錯誤。 若是你對如何完成這些工做感到好奇,能夠閱讀HTML規範中的「解析器中的錯誤處理和奇怪狀況介紹」部分。
網站一般使用圖像、CSS和JavaScript等外部資源。 這些文件須要從網絡或緩存中加載。 主線程能夠在解析構建DOM時會逐個請求它們,但爲了加快速度,「預加載掃描器」也會同時運行。 若是HTML文檔中存在<img>
或<link>
之類的內容,則預加載掃描器會檢查由HTML解析器生成的標記,並在瀏覽器進程中向網絡線程發送請求。
圖2:主線程解析HTML並構建DOM樹
當HTML解析器找到<script>
標記時,它會暫停解析HTML文檔,而且必須加載、解析和執行JavaScript代碼。 爲何要這樣處理? 由於JavaScript可使用像document.write()
那樣改變整個DOM結構的東西來改變文檔的形狀(HTML規範中的解析模型概述有一個很好的示意圖)。 這就是HTML解析器在從新解析HTML文檔以前必須等待JavaScript運行的緣由。 若是你對JavaScript執行中發生的事情感到好奇,V8團隊的博客對此進行了討論。
Web開發人員能夠經過多種方式向瀏覽器發送提示,以便很好地加載資源。 若是你的JavaScript不使用 document.write()
,則能夠向<script>
標記添加async
或defer
屬性。 而後,瀏覽器異步加載和運行JavaScript代碼,不會阻止解析。 若是合適,你也可使用JavaScript模塊。 <link rel ="preload">
是一種通知瀏覽器當前導航確定須要這個資源的方法,你但願儘快下載。 你能夠在資源優先級找到更多信息。
擁有DOM不足以知道頁面的外觀,由於咱們能夠在CSS中設置頁面元素的樣式。 主線程解析CSS並肯定每一個DOM節點的計算樣式。 這是有關基於CSS選擇器將哪一種樣式應用於每一個元素的信息。 你能夠在瀏覽器中開發者工具中的computed部分中看到此信息。
圖3:主線程解析CSS以添加計算樣式
即便你不提供任何CSS,每一個DOM節點都具備計算樣式。好比 <h1>
標籤的顯示要大於<h2>
標籤,同時爲每一個元素定義邊距。 這是由於瀏覽器具備默認樣式表。 若是你想知道Chrome的默認CSS是什麼樣的,你能夠在此處查看源代碼。
如今,渲染器進程知道每一個節點的文檔和樣式的結構,但這還不足以呈現頁面。 想象一下,你正試圖經過手機向朋友描述一幅畫: 「有一個大的紅色圓圈和一個小的藍色方塊」 這並不能徹底讓你的朋友瞭解這幅畫的外觀。
圖4:一我的站在一幅畫,經過電話線與另外一我的聯繫
佈局是查找元素幾何的過程。 主線程遍歷DOM並計算樣式和建立佈局樹,其中包含x y座標和邊界框大小等信息。 佈局樹能夠是與DOM樹相似的結構,但它僅包含與頁面上可見內容相關的信息。 若是display:none
,則該元素不是佈局樹的一部分(可是在佈局樹中包含visibility:hidden
的元素)。 相似地,若是應用具備相似p::before {content:"Hi!}
之類的內容的僞類,則它將包含在佈局樹中,即便它不在DOM中。
圖5:主線程經過DOM樹生成計算樣式和佈局樹
肯定頁面佈局是一項具備挑戰性的任務。 即便是最簡單的頁面佈局,如從上到下的塊流,也必須考慮字體的大小以及在哪裏劃分它們,由於它們會影響段落的大小和形狀; 而後影響下一段所需的位置。
CSS可使元素浮動到一側,掩蓋溢出項,並更改寫入方向。 你能夠想象,這個佈局階段是一項艱鉅的任務。 在Chrome項目中,有一個完整的工程師團隊負責佈局。 若是你想看到他們工做的細節,看看這些會議記錄很是有意思。
擁有了DOM、樣式和佈局仍然不足以呈現頁面。 假設你正在嘗試重現一幅畫。 你不只需知道元素的大小,形狀和位置,還須要判斷繪製它們的順序。
圖7:一個在畫布前拿着畫筆的人,正在思考是應該先畫圓圈仍是矩形
例如:能夠爲某些元素設置z-index
,在這種狀況下,按HTML中編寫的元素順序繪製將致使不正確的呈現。
圖8:頁面元素按HTML標記的順序出現,會致使錯誤的渲染圖像,由於沒有考慮z-index
在此繪製步驟中,主線程遍歷佈局樹以建立繪製記錄。 繪製記錄是繪製過程的一個註釋,如「背景優先,而後是文本,最後是矩形」。 若是你使用JavaScript繪製了<canvas>
元素,那麼可能對此過程很熟悉。
圖9:主線程遍歷佈局樹並生成繪製記錄
在渲染通道中最重要的一件事就是在每一個步驟中,前一個操做的結果被用於建立新數據。 例如:若是佈局樹中的某些內容發生更改,則須要爲文檔的受影響部分從新生成繪製順序。
若是要爲元素設置動畫,則瀏覽器必須在每一個幀之間運行這些操做。 咱們的大多數顯示器每秒刷新屏幕60次(60 fps); 當你在每一幀移動屏幕時,動畫對人眼來講會很平滑。 可是若是動畫錯過了其中的幀,則頁面將發生閃爍。
圖11:時間軸上的動畫幀
即便你的渲染操做可以跟上屏幕刷新,這些計算也是在主線程上運行的,這意味着當你的應用運行 JavaScript 時它可能會被阻止。
圖12:時間軸上的動畫幀,但JavaScript阻止了一幀
你能夠將JavaScript操做劃分爲小塊,並使用 requestAnimationFrame()
安排在每一個幀上運行。 有關此主題的更多信息,請參閱優化JavaScript執行。 你也能夠在 Web Workers 中運行 JavaScript 來避免阻塞主線程。
圖13:在動畫幀的時間軸上運行的較小的JavaScript塊
如今瀏覽器知道文檔的結構,每一個元素的樣式,頁面的幾何形狀和繪製順序,它是如何繪製頁面的? 將此信息轉換爲屏幕上的像素稱爲光柵化。
圖14:簡單光柵化過程
也許處理這種狀況的一種簡單的方法是在視口(viewport)內部使用柵格部件。 若是用戶滾動頁面,則移動光柵幀,並經過更多光柵填充缺乏的部分。 這就是Chrome首次發佈時處理柵格化的方式。 可是,現代瀏覽器運行一個稱爲合成的更復雜的過程。
合成是一種將頁面的各個部分分層,分別柵格化,並在一個被稱爲合成器線程的獨立線程中合成爲頁面的技術。 若是發生滾動,因爲圖層已經被柵格化,因此它所要作的就是合成一個新幀。 經過移動圖層和合成新幀,能夠用相同的方式實現動畫。
你可使用瀏覽器開發者工具的「layout」面板中查看你的網站如何劃分爲多個圖層。
爲了找出哪些元素須要放在哪些層中,主線程經過遍歷佈局樹以建立層樹(此部分在DevTools性能面板中稱爲「Update Layer Tree」)。 若是頁面某些應該是單獨圖層(如滑入式側面菜單)的部分可是沒有分配到圖層,那麼你可使用CSS中的will-change
屬性提示瀏覽器。
圖16:主線程生經過遍歷佈局樹來成層樹
也許你想要爲每一個元素提供圖層,可是過多的圖層進行合成可能會致使比每幀光柵化頁面的小部分更慢的操做,所以測量應用程序的渲染性能相當重要。 有關主題的更多信息,請參閱Stick to Compositor-Only Properties and Manage Layer Count
一旦建立了層樹並肯定了繪製順序,主線程就會將該信息提交給合成器線程。 合成器線程而後柵格化每一個圖層。 一個圖層可能像頁面的整個長度同樣大,所以合成器線程會將它們分紅圖塊,並將每一個圖塊發送到光柵線程。 柵格線程柵格化每個tile並將它們存儲在GPU內存中。
圖17:柵格線程建立tile位圖併發送到GPU
合成器線程能夠優先考慮不一樣的aster線程,以便視口(或附近)內的事物能夠先被光柵化。 圖層還具備多個不一樣分辨率的傾斜度,能夠處理放大操做等內容。
一旦tile被光柵化,合成器線程會收集稱爲繪製四邊形(draw quads )的tile信息來建立合成器幀(compositor frame)。
繪製四邊形 | 包含信息,例如圖塊在內存中的位置以及在考慮頁面合成的狀況下繪製圖塊的頁面中的位置。 |
---|---|
合成器幀 | 表示頁面幀的繪製四邊形的集合。 |
而後經過IPC將合成器幀提交給瀏覽器進程。這時能夠從UI線程添加另外一個合成器幀以用於瀏覽器UI更改,或者從其餘渲染器進程添加擴充數據。 這些合成器幀被髮送到GPU用來在屏幕上顯示。 若是發生滾動事件,合成器線程會建立另外一個合成器幀併發送到GPU。
圖18:合成器線程建立合成幀。 幀先被髮送到瀏覽器進程,而後再發送到GPU
合成的好處是它能夠在不涉及主線程的狀況下完成。 合成線程不須要等待樣式計算或 JavaScript 執行。 這就是合成動畫是平滑性能的最佳選擇的緣由。 若是須要再次計算佈局或繪圖,則必須涉及主線程。
在本文中,咱們研究了從解析到合成的渲染通道。
在本系列的下一篇文章中,咱們將更詳細地介紹合成器線程,並瞭解當用戶進行鼠標移動和單擊等操做時會發生什麼。