想要成爲一名合格的前端工程師,掌握相關瀏覽器的工做原理是必備的,這樣子纔會有一個完整知識體系,要是「能參透瀏覽器的工做原理,你就能解決80%的前端難題」。css
「其餘文章:」html
❝❞
[實用👍]推薦一些很是棒的前端網站 [乾貨👍]從詳細操做js數組到淺析v8中array.js [誠意滿滿👍]Chrome DevTools調試小技巧,效率➡️🚀🚀🚀 [建議👍]再來100道JS輸出題酸爽繼續(共1.8W字+鞏固JS基礎) [1.2W字👍]寫給女朋友的祕籍-瀏覽器工做原理(上)篇 面試如何寫出一個滿意的深拷貝(適合初級前端)
「這篇文章準備梳理一下渲染流程,也就是瀏覽器是怎麼把HTML,CSS,JS,圖片等資源最後顯示漂亮的頁面。」前端
❝仍是那句話,「瞭解瀏覽器是如何工做的,能讓你站在更高維度去理解前端」html5
❞
❝但願經過這篇文章,可以讓你從新認識瀏覽器,並把JavaScript,網絡,頁面渲染,瀏覽器安全等知識串聯起來,從而讓你對整個前端體系有全新的認識。web
❞
「這篇主要是梳理了渲染流程中幾個重要的步驟,以及從中有哪些優化的點,怎麼樣避免和減小重繪、重排,對優化性能上有必定的幫助。」面試
「讀完這一期內容,你將收穫」算法
小聲說:歡迎在留言區與我分享你的想法,也歡迎你在留言區記錄你的思考過程👊chrome
講瀏覽器中渲染流程,先把上一篇所梳理的內容大概回顧一下segmentfault
上一篇文章:[1.2W字👍]寫給女朋友的祕籍-瀏覽器工做原理(上)篇api
Http請求的總體流程,大體分爲八個階段
八個階段歸納爲: 構建請求 查找緩存 準備 IP 和端口 等待 TCP 隊列 創建 TCP 鏈接 發起 HTTP 請求 服務器處理請求 服務器返回請求和斷開鏈接
每一個階段大體的工做原理梳理了一下
瀏覽器緩存順帶提起了一下(面試常考) 性能優化一部分
渲染進程接受到CommitNavigation消息以後,便開始與網絡進程創建數據管道提及,此時渲染進程開始幹活了
那麼纔有了咱們接下來要梳理的內容,渲染進程如何工做的呢
首先要了解的概念:
渲染引擎:它是瀏覽器最核心的部分是 「Rendering Engine」,不過咱們通常習慣將之稱爲 「瀏覽器內核」
渲染引擎主要包括的線程:
各個線程主要職責
以上來自阿寶哥總結:你不知道的 Web Workers (上)[7.8K 字 | 多圖預警]
有了上述的概念,對接下咱們講渲染流水線會有所幫助
好久以前就把瀏覽器工做原理讀完了,看了不少博客,文章,當時簡簡單單的梳理一些內容,以下👇
簡略版渲染機制通常分爲如下幾個步驟
接下來大概就是這麼說:
在構建 CSSOM 樹時,會阻塞渲染,直至 CSSOM 樹構建完成。而且構建 CSSOM 樹是一個十分消耗性能的過程,因此應該儘可能保證層級扁平,減小過分層疊,越是具體的 CSS 選擇器,執行速度越慢。
當 HTML 解析到 script 標籤時,會暫停構建 DOM,完成後纔會從暫停的地方從新開始。也就是說,若是你想首屏渲染的越快,就越不該該在首屏就加載 JS 文件。而且 CSS 也會影響 JS 的執行,只有當解析完樣式表纔會執行 JS,因此也能夠認爲這種狀況下,CSS 也會暫停構建 DOM。
說完這些,記下來就講幾個面試經常會提起的,會問你的知識點👇
Load 事件觸發表明頁面中的 DOM,CSS,JS,圖片已經所有加載完畢。
DOMContentLoaded 事件觸發表明初始的 HTML 被徹底加載和解析,不須要等待 CSS,JS,圖片加載。
通常來講,能夠把普通文檔流當作一個圖層。特定的屬性能夠生成一個新的圖層。不一樣的圖層渲染互不影響,因此對於某些頻繁須要渲染的建議單獨生成一個新圖層,提升性能。但也不能生成過多的圖層,會引發副作用。
經過如下幾個經常使用屬性能夠生成新圖層
translate3d
、translateZ
will-change
video
、iframe
標籤opacity
動畫轉換position: fixed
重繪和迴流是渲染步驟中的一小節,可是這兩個步驟對於性能影響很大。
color
就叫稱爲重繪迴流一定會發生重繪,重繪不必定會引起迴流。迴流所需的成本比重繪高的多,改變深層次的節點極可能致使父節點的一系列迴流。
因此如下幾個動做可能會致使性能問題:
不少人不知道的是,重繪和迴流其實和 Event loop 有關。
resize
或者 scroll
,有的話會去觸發事件,因此 resize
和 scroll
事件也是至少 16ms 纔會觸發一次,而且自帶節流功能。requestAnimationFrame
回調IntersectionObserver
回調,該方法用於判斷元素是否可見,能夠用於懶加載上,可是兼容性很差requestIdleCallback
回調。以上內容來自於 HTML 文檔
使用 translate
替代 top
<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引發迴流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>
複製代碼
使用 visibility
替換 display: none
,由於前者只會引發重繪,後者會引起迴流(改變了佈局)
把 DOM 離線後修改,好比:先把 DOM 給 display:none
(有一次 Reflow),而後你修改100次,而後再把它顯示出來
不要把 DOM 結點的屬性值放在一個循環裏當成循環裏的變量
for(let i = 0; i < 1000; i++) {
// 獲取 offsetTop 會致使迴流,由於須要去獲取正確的值
console.log(document.querySelector('.test').style.offsetTop)
}
複製代碼
不要使用 table 佈局,可能很小的一個小改動會形成整個 table 的從新佈局
動畫實現的速度的選擇,動畫速度越快,迴流次數越多,也能夠選擇使用 requestAnimationFrame
CSS 選擇符從右往左匹配查找,避免 DOM 深度過深
將頻繁運行的動畫變爲圖層,圖層可以阻止該節點回流影響別的元素。好比對於 video
標籤,瀏覽器會自動將該節點變爲圖層。
上面的渲染流程是我一年前就在我筆記中存在的內容,我還記得當時學習的方式是囫圇吞棗式的,上面☝簡略版的渲染流程,我印象中是在GitHub上面某博看看的,當時直接Copy下來的,當時以爲這個渲染原理這塊有了別人梳理好的結論,本身多看看,會記住的,事實上,面試的時候,提起這部分的時候,深度明顯不夠,天然就被問倒了
下來梳理了一份詳細的版本,坦白說,做爲一個學者,天然是站在巨人的肩膀上,去總結梳理知識,我認爲這是對我最有效的學習方式
讓我帶着你🚗重溫通常渲染流程吧
較爲專業的術語總結爲如下階段:
你能夠想象一下,從0,1字節流到最後頁面展示在你面前,這裏面渲染機制確定很複雜,因此渲染模塊把執行過程當中化爲不少的子階段,渲染引擎從網絡進程拿到字節流數據後,通過這些子階段的處理,最後輸出像素,這個過程能夠稱爲渲染流水線
,咱們從一張圖上來看👇
那接下來就從每一個階段來梳理一下大體過程。
這個過程主要工做就是講HTML內容轉換爲瀏覽器DOM樹結構
文檔對象模型(DOM)
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>
複製代碼
咱們先看看數據是怎麼樣轉換的👇
大概過程:
咱們把上述這樣子的過程就叫作是構建DOM樹過程
這個子階段主要有三個步驟
咱們拿到的也就是0,1字節流數據,瀏覽器沒法直接去識別的,因此渲染引擎收到CSS文本數據後,會執行一個操做,轉換爲瀏覽器能夠理解的結構-styleSheets
若是你很想了解這個格式化的過程,能夠好好去研究下,不一樣的瀏覽器可能在CSS格式化過程當中會有所不一樣,在這裏就不展開篇幅了。
經過瀏覽器的控制檯document.styleSheets
能夠來查看這個最終結果。經過JavaScript能夠完成查詢和修改功能,或者說這個階段爲後面的樣式操做提供基石。
什麼是標準化樣式表呢?先看一段CSS文本👇
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
複製代碼
有些時候,咱們寫CSS 樣式的時候,會寫font-size:2em;color:red;font-weight:bold
,像這些數值並不容易被渲染引擎所理解,所以須要在計算樣式以前將它們標準化,如em
->px
,red
->rgba(255,0,0,0)
,bold
->700
等等。
上面的代碼標準後屬性值是什麼樣子呢👇
經過格式化
和標準化
,接下來就是計算每一個節點具體樣式信息了。
計算規則:繼承
和層疊
繼承
:每一個子節點會默認去繼承父節點的樣式,若是父節點中找不到,就會採用瀏覽器默認的樣式,也叫UserAgent樣式
。
層疊
:樣式層疊,是CSS一個基本特徵,它定義如何合併來自多個源的屬性值的算法。某種意義上,它處於核心地位,具體的層疊規則屬於深刻 CSS 語言的範疇,這裏就補展開篇幅說了。
不過值得注意的是,在計算完樣式以後,全部的樣式值會被掛在到window.getComputedStyle
當中,也就是能夠經過JS來獲取計算後的樣式,很是方便。
這個階段,完成了DOM節點中每一個元素的具體樣式,計算過程當中要遵循CSS的繼承
和層疊
兩條規則,最終輸出的內容是每一個節點DOM的樣式,被保存在ComputedStyle中。
想了解每一個 DOM 元素最終的計算樣式,能夠打開 Chrome 的「開發者工具」,選擇第一個「element」標籤,好比我下面就選擇了div標籤,而後再選擇「Computed」子標籤,以下圖所示:
若是不是很理解的話,能夠看這裏👇
跟處理HTML同樣,咱們須要更具CSS兩個規則:繼承
和層疊
轉換成某種瀏覽器能理解和處理的東西,處理過程相似處理HTML,如上圖☝
CSS 字節轉換成字符,接着轉換成令牌和節點,最後連接到一個稱爲「CSS 對象模型」(CSSOM) 的樹結構內👇
不少人確定看這個很熟悉,確實,不少博客都是基於CSSOM說法來說的,我要說的是:
和DOM不同,源碼中並無CSSOM這個詞,因此不少文章說的CSSOM應該就是styleSheets,固然了這個styleSheets咱們能夠打印出來的
不少文章說法是渲染樹也是16年前的說法,如今代碼重構了,咱們能夠把LayoutTree當作是渲染樹,不過它們之間仍是有些區別的。
上述過程已經完成DOM樹(DOM樹)構建,以及樣式計算(DOM樣式),接下來就是要經過瀏覽器的佈局系統肯定元素位置,也就是生成一顆佈局樹(Layout Tree),以前說法叫 渲染樹。
在DOM樹上不可見的元素,head元素,meta元素等,以及使用display:none屬性的元素,最後都不會出如今佈局樹上,因此瀏覽器佈局系統須要額外去構建一棵只包含可見元素佈局樹。
咱們直接結合圖來看看這個佈局樹構建過程:
爲了構建佈局樹,瀏覽器佈局系統大致上完成了下面這些工做:
接下來就是要計算佈局樹節點的座標位置,佈局的計算過程很是複雜,張開介紹的話,會顯得文章過於臃腫,大多數狀況下,咱們只須要知道它所作的工做是什麼,想知道它是如何作的話,能夠看看如下兩篇文章👇
一圖歸納上面三個階段
首先須要知道的就是,瀏覽器在構建完佈局樹
後,還須要進行一系列操做,這樣子可能考慮到一些複雜的場景,好比一些些複雜的 3D 變換、頁面滾動,或者使用 z-indexing 作 z 軸排序等,還有好比是含有層疊上下文如何控制顯示和隱藏等狀況。
生成圖層樹
你最終看到的頁面,就是由這些圖層一塊兒疊加構成的,它們按照必定的順序疊加在一塊兒,就造成了最終的頁面。
瀏覽器的頁面實際上被分紅了不少圖層,這些圖層疊加後合成了最終的頁面。
咱們來看看圖層與佈局樹之間關係,以下圖👇
一般狀況下,並非佈局樹的每一個節點都包含一個圖層,若是一個節點沒有對應的層,那麼這個節點就從屬於父節點的圖層。
那什麼狀況下,渲染引擎會爲特定的節點建立新圖層呢?
有兩種狀況須要分別討論,一種是顯式合成,一種是隱式合成。
顯式合成
1、 擁有層疊上下文的節點。
層疊上下文也基本上是有一些特定的CSS屬性建立的,通常有如下狀況:
2、須要剪裁(clip)的地方。
好比一個標籤很小,50*50像素,你在裏面放了很是多的文字,那麼超出的文字部分就須要被剪裁。固然若是出現了滾動條,那麼滾動條也會被單獨提高爲一個圖層,以下圖
數字1箭頭指向的地方,能夠看看,可能效果不是很明顯,你們能夠本身打開這個Layers探索下。
元素有了層疊上下文的屬性或者須要被剪裁,知足其中任意一點,就會被提高成爲單獨一層。
隱式合成
這是一種什麼樣的狀況呢,通俗意義上來講,就是z-index
比較低的節點會提高爲一個單獨的途圖層,那麼層疊等級比它高
的節點都會成爲一個獨立的圖層。
缺點: 根據上面的文章來講,在一個大型的項目中,一個z-index
比較低的節點被提高爲單獨圖層後,層疊在它上面的元素通通都會提高爲單獨的圖層,咱們知道,上千個圖層,會增大內存的壓力,有時候會讓頁面崩潰。這就是層爆炸
完成了圖層的構建,接下來要作的工做就是圖層的繪製了。圖層的繪製跟咱們平常的繪製同樣,每次都會把一個複雜的圖層拆分爲很小的繪製指令,而後再按照這些指令的順序組成一個繪製列表,相似於下圖👇
從圖中能夠看出,繪製列表中的指令其實很是簡單,就是讓其執行一個簡單的繪製操做,好比繪製粉色矩形或者黑色的線等。而繪製一個元素一般須要好幾條繪製指令,由於每一個元素的背景、前景、邊框都須要單獨的指令去繪製。
你們能夠在 Chrome 開發者工具中在設置欄中展開 more tools
, 而後選擇Layers
面板,就能看到下面的繪製列表:
在該圖中,**箭頭2指向的區域 **就是 document 的繪製列表,**箭頭3指向的拖動區域 **中的進度條能夠重現列表的繪製過程。
固然了,繪製圖層的操做在渲染進程中有着專門的線程,這個線程叫作合成線程。
接下來咱們就要開始繪製操做了,實際上在渲染進程中繪製操做是由專門的線程來完成的,這個線程叫合成線程。
繪製列表準備好了以後,渲染進程的主線程會給合成線程
發送commit
消息,把繪製列表提交給合成線程。接下來就是合成線程一展宏圖的時候啦。
你想呀,有時候,你的圖層很大,或者說你的頁面須要使用滾動條,而後頁面的內容太多,多的沒法想象,這個時候須要滾動很久才能滾動到底部,可是經過視口,用戶只能看到頁面的很小一部分,因此在這種狀況下,要繪製出全部圖層內容的話,就會產生太大的開銷,並且也沒有必要。
首屏渲染加速能夠這麼理解:
由於後面圖塊(非視口內的圖塊)數據要進入 GPU 內存,考慮到瀏覽器內存上傳到 GPU 內存的操做比較慢,即便是繪製一部分圖塊,也可能會耗費大量時間。針對這個問題,Chrome 採用了一個策略: 在首次合成圖塊時只採用一個低分辨率的圖片,這樣首屏展現的時候只是展現出低分辨率的圖片,這個時候繼續進行合成操做,當正常的圖塊內容繪製完畢後,會將當前低分辨率的圖塊內容替換。這也是 Chrome 底層優化首屏加載速度的一個手段。
接着上面的步驟,有了圖塊以後,合成線程會按照視口附近的圖塊來優先生成位圖,實際生成位圖的操做是由柵格化來執行的。所謂柵格化,是指將圖塊轉換爲位圖。
合成線程
運行方式以下👇
一般,柵格化過程都會使用 GPU 來加速生成,使用 GPU 生成位圖的過程叫快速柵格化,或者 GPU 柵格化,生成的位圖被保存在 GPU 內存中。
相信你還記得,GPU 操做是運行在 GPU 進程中,若是柵格化操做使用了 GPU,那麼最終生成位圖的操做是在 GPU 中完成的,這就涉及到了跨進程操做。具體形式你能夠參考下圖:
從圖中能夠看出,渲染進程把生成圖塊的指令發送給 GPU,而後在 GPU 中執行生成圖塊的位圖,並保存在 GPU 的內存中。
柵格化操做完成後,合成線程會生成一個繪製命令,即"DrawQuad",併發送給瀏覽器進程。
瀏覽器進程中的viz組件
接收到這個命令,根據這個命令,把頁面內容繪製到內存,也就是生成了頁面,而後把這部份內存發送給顯卡,那你確定對顯卡的原理很好奇。
看了某博主對顯示器顯示圖像的原理解釋:
不管是 PC 顯示器仍是手機屏幕,都有一個固定的刷新頻率,通常是 60 HZ,即 60 幀,也就是一秒更新 60 張圖片,一張圖片停留的時間約爲 16.7 ms。而每次更新的圖片都來自顯卡的前緩衝區。而顯卡接收到瀏覽器進程傳來的頁面後,會合成相應的圖像,並將圖像保存到後緩衝區,而後系統自動將
前緩衝區
和後緩衝區
對換位置,如此循環更新。
這個時候,心中就有點概念了,好比某個動畫大量佔用內存時,瀏覽器生成圖像的時候會變慢,圖像傳送給顯卡就會不及時,而顯示器仍是以不變的頻率刷新,所以會出現卡頓,也就是明顯的掉幀現象。
用一張圖來總結👇
咱們把上面整個的渲染流水線,用一張圖片更直觀的表示👇
更新視圖三種方式
另一個叫法是重排,迴流觸發的條件就是:對 DOM 結構的修改引起 DOM 幾何尺寸變化的時候,會發生迴流
過程。
具體一點,有如下的操做會觸發迴流:
width
、height
、padding
、margin
、left
、top
、border
等等, 這個很好理解。增減
或者移動
。offset
族、scroll
族和client
族屬性的時候,瀏覽器爲了獲取這些值,須要進行迴流操做。window.getComputedStyle
方法。一些經常使用且會致使迴流的屬性和方法:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
依照上面的渲染流水線,觸發迴流的時候,若是 DOM 結構發生改變,則從新渲染 DOM 樹,而後將後面的流程(包括主線程以外的任務)所有走一遍。
當頁面中元素樣式的改變並不影響它在文檔流中的位置時(例如:color
、background-color
、visibility
等),瀏覽器會將新樣式賦予給元素並從新繪製它,這個過程稱爲重繪。
根據概念,咱們知道因爲沒有致使 DOM 幾何屬性的變化,所以元素的位置信息不須要更新,從而省去佈局的過程,流程以下:
跳過了佈局樹
和建圖層樹
,直接去繪製列表,而後在去分塊,生成位圖等一系列操做。
能夠看到,重繪不必定致使迴流,但迴流必定發生了重繪。
還有一種狀況:就是更改了一個既不要佈局也不要繪製的屬性,那麼渲染引擎會跳過佈局和繪製,直接執行後續的合成操做,這個過程就叫合成。
舉個例子:好比使用CSS的transform來實現動畫效果,避免了迴流跟重繪,直接在非主線程中執行合成動畫操做。顯然這樣子的效率更高,畢竟這個是在非主線程上合成的,沒有佔用主線程資源,另外也避開了佈局和繪製兩個子階段,因此相對於重繪和重排,合成能大大提高繪製效率。
利用這一點好處:
提高合成層的最好方式是使用 CSS 的 will-change 屬性
好比利用 CSS3 的transform
、opacity
、filter
這些屬性就能夠實現合成的效果,也就是你們常說的GPU加速。
非主線程
處理部分,即直接交給合成線程
處理。GPU
優點,合成線程生成位圖的過程當中會調用線程池,並在其中使用GPU
進行加速生成,而GPU 是擅長處理位圖數據的。createDocumentFragment
進行批量的 DOM 操做transform: translateZ(0);
你不知道的 Web Workers (上)[7.8K 字 | 多圖預警]
How Browsers Work: Behind the scenes of modern web browsers
CSS GPU Animation: Doing It Right
本文使用 mdnice 排版