渲染進程的內部工作
這是我們了解瀏覽器如何工作4篇博客的第3篇。之前奸柬,我們介紹了 多進程架構 和 導航流程鹉梨。在本文中,我們將研究渲染進程內部發(fā)生了什么序攘。
渲染器進程涉及web優(yōu)化的許多方面茴她。由于渲染進程內部發(fā)生了很多事情,因此本文只是概述程奠。如果你想更加深入丈牢,“web基礎知識的性能優(yōu)化部分”有更多的資源。
渲染進程處理web內容
渲染進程負責處理tab選項卡內發(fā)生的所有事情瞄沙。在一個渲染進程中己沛,主進程處理你發(fā)送給用戶的大多數(shù)代碼慌核。如果有時候你使用web worker 或者 service worker,你的部分 javascript 由woker線程處理申尼。排版和光柵線程也在渲染進程內部運行垮卓,以便高效,流暢的渲染頁面师幕。
渲染進程的核心工作是將html粟按、css和javascript轉換為可以與用戶交互的web頁面。
![renderer.png](https://upload-images.jianshu.io/upload_images/4046496-1358c141b55c6a19.png&originHeight=455&originWidth=865&originalType=binary&size=56422&status=done&style=none&width=865?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
解析
DOM構造
當渲染進程接收到導航提交的信息霹粥,并且開始接收html數(shù)據(jù)灭将,主線程開始解析文本字符(html),并且將其轉換為文檔對象模型(DOM)后控。
DOM是一個瀏覽器對頁面的內部表示庙曙,也是web開發(fā)者可以通過javascript與之交互的數(shù)據(jù)結構和API。
解析一個HTML文檔到DOM是通過HTML標準來定義的浩淘。你可能注意到將HTML放到瀏覽器從來沒有拋出過錯誤捌朴。例如,缺少閉合 **</p> **是一個合法的HTML张抄。錯誤的標記例如 **Hi! <b>I'm <i>Chrome</b>!</i> **(b標簽在i標簽之前關閉)男旗,它被看成是 Hi! <b>I'm <i>Chrome</i></b><i>!</i>。這是因為HTML規(guī)范定義了如何優(yōu)雅的處理這些錯誤欣鳖。如果你關心這些事情是如何完成的,你可以閱讀HTML規(guī)范中“錯誤處理和解析中奇怪的例子”一節(jié)茴厉。
子資源加載
一個網(wǎng)站通常會使用額外的資源泽台,例如圖片、css和javascript矾缓。這些文件需要從網(wǎng)絡或者緩存中加載怀酷。主進程可以 在解析構建DOM的時候一個接一個的請求他們,但為了加快速度嗜闻,“預加載掃描”會同時運行蜕依。如果在HTML里面有一些像img和link的內容,預加載會查看HTML解析生成的token琉雳,并且在瀏覽器進程中發(fā)送網(wǎng)絡請求样眠。
![dom.png](https://upload-images.jianshu.io/upload_images/4046496-0100cd8ea5b88f3f.png&originHeight=455&originWidth=865&originalType=binary&size=68733&status=done&style=none&width=865?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
javascript會阻塞解析
當HTML解析發(fā)現(xiàn)一個script標簽,它停止解析HTML文檔翠肘,并且去加載檐束,解析,并且執(zhí)行javascript代碼束倍。為什么被丧?因為javascript可以改變文檔的形狀盟戏,使用document.write()可以改變整個的DOM結構(解析模型概述在HTML定義里有一個好的繪圖)。這是HTML解析為什么在恢復HTML文檔解析之前等待javascript執(zhí)行甥桂。如果你像深入了解javascript執(zhí)行發(fā)生了什么柿究,V8團隊關于此的討論在這。
提示瀏覽器如何加載資源
web開發(fā)者有很多種方式可以發(fā)送提示給瀏覽器按序加載資源黄选。如果你的javascript沒有使用document.write()蝇摸,你可以添加 async 和 defer 屬性給 <script> 標簽。瀏覽器可以異步地加載和執(zhí)行 javascript 代碼糕簿,并且不會阻塞html解析探入。如果適合的話你可能也會使用 javascript module。<link rel="preload">是一種通知瀏覽器在當前導航需要盡可能早的下載資源的方式懂诗。你可以閱讀更多的內容在這里 Resource Prioritization – Getting the Browser to Help You.
樣式計算
只有一個DOM是不足以知道頁面長什么樣子的蜂嗽,因為我們可以在css中設置頁面樣式。主線程解析CSS并且為每個DOM節(jié)點準確地計算出樣式殃恒。這是基于CSS選擇器為每個元素應用對應樣式的信息植旧。你可以在 DevTools 中的 computed 部分看到這些信息。
![computedstyle.png](https://upload-images.jianshu.io/upload_images/4046496-bb3a189e25497a78.png&originHeight=455&originWidth=865&originalType=binary&size=62001&status=done&style=none&taskId=uaa34cadf-82e6-4854-bdee-e494bf0e6ed?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
即使你沒有提供任何的CSS离唐,每個DOM節(jié)點也會又一個computed樣式病附。<h1>標簽比<h2>標簽展示出來大,并且為每個元素都定義了外間距亥鬓。這是因為瀏覽器有一個默認樣式表完沪。如果你想知道chrome有哪些默認的css,請查看這個chrome默認css嵌戈。
布局
現(xiàn)在渲染進程知道文檔的結構和每個節(jié)點的樣式覆积,但是這些還不足以渲染一個頁面。想象一下你在電話里面為你的朋友描述一副畫熟呛】淼担“這里有一個大紅色的圓和一個小的藍色正方形”這些信息不能夠讓你的朋友準確的知道畫到底是什么樣子。
![tellgame.png](https://upload-images.jianshu.io/upload_images/4046496-9818278ab420c316.png&originHeight=461&originWidth=865&originalType=binary&size=58066&status=done&style=none&taskId=u6c48c81c-369d-471e-a688-e62853dfa11&width=510?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
布局是一個尋找元素坐標的過程庵朝。主線程遍歷DOM和計算樣式吗冤,創(chuàng)建包含x,y坐標和邊界框大小的布局樹九府。布局樹和DOM樹有著相似的結構椎瘟,但是它僅僅包含頁面上可見元素的關聯(lián)信息。如果應用了 display:none侄旬,那么這個元素就不是布局樹的一部分(然而降传,一個visibility:hidden的元素在布局樹中)。類似地勾怒,一個包含內容的偽類就像p::before{content: "Hi!"}被應用婆排,它會包含在布局樹中声旺,即使它不在DOM中。
![layout.png](https://upload-images.jianshu.io/upload_images/4046496-00df248361aa5309.png&originHeight=455&originWidth=865&originalType=binary&size=61301&status=done&style=none&taskId=u98c64c8a-bf82-48ba-9c7c-f06c32c52cd?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
確定頁面布局是一個有挑戰(zhàn)的任務段只。即使最簡單的頁面腮猖,從上到下塊布局,也必須去考慮字體多大赞枕,哪里需要換行澈缺,因為這些都會影響段落的大小和形狀;而且也會影響下一行的段落的內容炕婶。
CSS可以使元素浮動到一邊姐赡,屏蔽溢出項,改變書寫的方向柠掂。你可以想象项滑,這個布局階段有一個艱巨的任務。在Chrome中涯贞,一個工程師團隊都在為布局工作枪狂。如果你想了解更多他們的工作細節(jié),請點擊 演講視頻 觀看有趣的記錄宋渔。
繪制
有了DOM州疾,樣式,布局還是不足以渲染一個頁面皇拣。假設你想模仿一幅畫严蓖。你知道大小,形狀氧急,元素的位置谈飒,但是你仍然需要判斷繪制的順序。
![drawgame.png](https://upload-images.jianshu.io/upload_images/4046496-bdff3ae1d1e4717c.png&originHeight=324&originWidth=865&originalType=binary&size=33042&status=done&style=none&taskId=ua2f185d8-d356-47e6-9315-17c781c4d39&width=615?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
例如态蒂,會為一些元素設置z-index,在下面的例子中费什,按照HTML的順序繪制將會出現(xiàn)錯誤的渲染結果钾恢。
![zindex.png](https://upload-images.jianshu.io/upload_images/4046496-85bd13032539ca56.png&originHeight=963&originWidth=1802&originalType=binary&size=100551&status=done&style=none&taskId=ua0dc7c6c-4034-4cde-90b3-264afbf28b0&width=736?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
圖8:頁面元素按照HTML標記的順序出現(xiàn),由于未考慮 z-index導致生成錯誤的渲染圖
在當前繪制步驟鸳址,主線程遍歷布局樹去生成繪制記錄瘩蚪。繪制記錄是對繪制過程的記錄,例如“先背景稿黍,后文字疹瘦,最后矩形”。如果你使用 javascript 在<canvas>元素上繪制巡球,這個過程對你就比較熟悉言沐。
![paint.png](https://upload-images.jianshu.io/upload_images/4046496-ecfb603181a342ad.png&originHeight=455&originWidth=865&originalType=binary&size=53882&status=done&style=none&taskId=u2752431b-149a-4f27-8ee4-4514c2dff88&width=716?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
圖9:主線程遍歷布局樹并且生成繪制記錄
更新渲染流非常昂貴
在渲染流中要掌握的最重要的一點是邓嘹,在每一步中,使用上一步操作的結果去生成新數(shù)據(jù)险胰。例如汹押,如果布局樹中的某些發(fā)生改變,需要為受影響的文檔部分重新生成繪制順序起便。
如果為某些元素設置動畫棚贾,瀏覽器必須在每個frame中間去運行這些操作。我們大多數(shù)的顯示器都是每秒鐘刷新60次屏幕榆综;當你在每一幀中在屏幕上移動物體時妙痹,動畫對于人眼的顯示都是平滑的。然而鼻疮,如果錯過了它們之間的幀怯伊,則頁面將顯示為“混亂”。
![pagejank1.png](https://upload-images.jianshu.io/upload_images/4046496-34f15bee037af70d.png&originHeight=231&originWidth=1000&originalType=binary&size=34756&status=done&style=none&taskId=ua36e3984-27ce-4186-8fdd-4bd7e781cb1?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
圖11:時間軸上的動畫幀
即使渲染操作域屏幕刷新保持一致陋守,這些計算仍然運行在主線程上震贵,這也意味著當你的應用運行javascript時會阻塞。
![pagejank2.png](https://upload-images.jianshu.io/upload_images/4046496-a11465d1b7f5ab7d.png&originHeight=231&originWidth=1000&originalType=binary&size=28617&status=done&style=none&taskId=u70ea8af2-c123-475b-8241-1baceb7bf7e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
圖12:時間軸上動畫幀水评,但一個幀被javascript阻止
你可以將Javascript操作分成小塊猩系,并使用requestAnimationFrame()安排在每個幀上運行。對于這個話題想要了解更多中燥,你可以看 js優(yōu)化執(zhí)行寇甸。你也可以讓你js運行在web woker上去避免阻塞主線程。
![raf.png](https://upload-images.jianshu.io/upload_images/4046496-fa819bc3e3ed7eb1.png&originHeight=231&originWidth=1000&originalType=binary&size=32639&status=done&style=none&taskId=ud5d9dd65-0394-491e-94a2-68822d5cf75?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
圖13:在帶有動畫幀的時間軸上運行javascript小塊
合成
你如何去畫一個頁面疗涉?
現(xiàn)在瀏覽器知道了文檔的結構拿霉,每個元素的樣式,在頁面里面的坐標和繪制順序咱扣,如何繪制一個頁面绽淘?將這些信息轉化為屏幕上的像素叫作光柵化(rasterizing)。
處理此問題的一個幼稚的方法就是在視口內部柵格化闹伪。如果用戶滾動頁面沪铭,則移動柵格化框架,并通過柵格化更多內容去填充缺失的部分偏瓤。這是Chrome第一次發(fā)布時處理柵格化的方式杀怠。然而,現(xiàn)代瀏覽器運行著更為復雜的過程厅克,叫作合成赔退。
什么是合成?
合成是將一個頁面的各個部分分成多個層,分別對其柵格化的技術硕旗,在一個叫作合成線程的獨立線程中合稱為一個頁面窗骑。如果發(fā)生滾動,由于圖層已經(jīng)被柵格化卵渴,所以就不得不合成一個新的幀慧域。可以使用相同的方式浪读,通過移動圖層生成一個新的幀來實現(xiàn)動畫昔榴。
你可以在開發(fā)者工具的“Layers panel”中查看你的網(wǎng)站如何被劃分為多個圖層。
劃分為圖層
為了找出哪些元素應該在哪些圖層碘橘,主線程遍歷布局樹為了生成圖層樹(在開發(fā)者工具中這部分在performance面板中被叫作“更新圖層樹”)互订。如果頁面的某些部分應該是獨立的層(例如滑入式側邊欄)沒有生成,你可以在css中使用 will-change 屬性去提示瀏覽器痘拆。
![layer.png](https://upload-images.jianshu.io/upload_images/4046496-42679de3aba690ea.png&originHeight=455&originWidth=865&originalType=binary&size=53972&status=done&style=none&taskId=u8b238f30-422a-450f-bea1-1b87a2f4067?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
你可能試圖為每個元素提供圖層仰禽,但是與頁面的每幀進行柵格化相比,過多數(shù)量的圖層合成會使得操作速度變慢纺蛆,因此衡量應用程序的渲染性能特別重要吐葵。想了解更多這個話題,可以查看 Stick to Compositor-Only Properties and Manage Layer Count桥氏。
柵格化和合成脫離主線程
一旦圖層樹生成佳恬,并且繪制順序確定惨奕,主線程會提交這些信息給合成線程寇窑。合成器線程會柵格化每個圖層惋增。一個圖層可以和整個頁面的長度一樣大,因此合成器會講他們劃分為圖塊堕伪,并且將每個圖塊發(fā)送給柵格線程揖庄。柵格線程柵格化每個圖塊,并且將他們存儲在GPU中欠雌。
![raster.png](https://upload-images.jianshu.io/upload_images/4046496-e616d122e234abf3.png&originHeight=455&originWidth=865&originalType=binary&size=71187&status=done&style=none&taskId=u8933faac-bd63-4836-ba13-e65900ecbde?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
合成器線程會優(yōu)先處理不同的柵格線程蹄梢,因此可以首先對視口柵格化。一個圖層還具有不同分辨率的平鋪富俄,用于處理放大縮小的操作禁炒。
一旦圖塊被柵格化,合成器線程將手機信息叫作“draw quads”生成一個“compositor frame”蛙酪。
draw quads | 包含例如圖塊在內存中位置以及考慮頁面合成的情況下在頁面中合成圖塊的信息。 |
---|---|
compositor frame | draw quads集合去展示一個頁面的框架 |
然后合成器框架通過IPC提交信息給瀏覽器進程翘盖。此時桂塞,可以從更改瀏覽器UI的UI線程或者另外一個用于擴展的渲染進程添加另一個合成器框架。這些合成框架都被發(fā)送到GPU用于顯示到屏幕上馍驯。如果滾動事件發(fā)生阁危,合成器進程創(chuàng)建另一個合成器框架發(fā)送給GPU玛痊。
![composit.png](https://upload-images.jianshu.io/upload_images/4046496-092f712d1bc9a6f7.png&originHeight=455&originWidth=865&originalType=binary&size=63713&status=done&style=none&taskId=u267d0d2c-cfd0-4474-a2c3-fe6cbca9f28?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
合成的好處是不涉及主線程就可以完成。合成器線程不需要等到樣式計算和Js執(zhí)行狂打。這是為什么合成動畫被認為是最佳的平滑優(yōu)化的原因擂煞。如果布局和繪制需要重新計算,則必須涉及主線程趴乡。
總結
在這篇文章中对省,我們深入了解了從解析到合成的渲染流。希望你現(xiàn)在可以獲取到更多的關于網(wǎng)站優(yōu)化的知識晾捏。
在下面也是本系列最后一篇文章中蒿涎,我們將更加深入了解合成器線程,并且知道當用戶輸入例如mouse move和click的時候發(fā)生了什么惦辛。