看了就會的瀏覽器幀原理
本文轉載自微信公眾號「TianTianUp」,作者TianTianUp。轉載本文請聯(lián)系TianTianUp公眾號。
前言
本文會介紹瀏覽器中幀(Frame)的概念,它的流程是怎么樣的。
至于寫這個文章的出發(fā)點在于,我好奇瀏覽器中像素工作流程是怎么樣的,什么時候開始的,最后的結果是什么。
基于這些好奇,查閱了些外文資料,本文提供了些參考,參考鏈接在文末。
本文會介紹瀏覽器中幀(Frame)的概念,它的流程是怎么樣的。
至于寫這個文章的出發(fā)點在于,我好奇瀏覽器中像素工作流程是怎么樣的,什么時候開始的,最后的結果是什么。
基于這些好奇,查閱了些外文資料,本文提供了些參考,參考鏈接在文末。
緣由
在將幀的概念前,我們得從背景開始看起,也就是渲染頁面的這個過程,有哪些關鍵性的路徑呢。
五大關鍵渲染路徑
像素輸出到頁面,肯定經歷了很多的過程,那我們作為前端工程師,工作中需要注意的點是哪些呢,這里給出參考:
渲染關鍵路徑
這五個主要的部分,應該是我們值得去關注的,因為我們擁有最大控制權的部分。至于每一個過程具體是怎么樣的呢,不清楚的可以參考下圖:
每個階段任務
所以在這么一個像素的管道里,每部分都有可能造成卡頓,所以我們需要額外的關注這些,畢竟那一部分不當,都會開了不必要的性能開銷。
三種輸出方式
當時我的疑問是: 難道每一幀都總是會經過管道每個部分的處理嘛,其實不是這樣子的,從視覺的角度來說,管道針對指定幀的運行通常有三種方式:
指定幀的運行通常有三種方式
如果我們以第三種方式來更新視圖,也就是更改一個既不要布局也不要繪制的屬性,則瀏覽器將跳到只執(zhí)行合成。
跑個demo
為了更加具體的驗證上述的過程,可以動手跑一個demo,來驗證一下。
demo地址:https://googlechrome.github.io/devtools-samples/jank/
主線程
我們添加多個dom元素進行動畫,效果更佳明顯,接著我們打開Performance,Record這個過程,我們需要關注的是Main選項卡,也就是主線程,我們在放大里面的Task,就有了下圖:
詳細的Task
經歷的過程,也是很清楚看到,Update Layer Tree -->> Layout -->> Paint -->> Composite Layers。
如果你不是很清楚Performance中名稱的含義,可以參考下面這篇文章,點這里:
https://mp.weixin.qq.com/s/iodsGPWgYc97yWLb09Xk6A
接著,我們按下,Optimize按鈕,按照之前的流程走,Record后,發(fā)現(xiàn)不對勁,還是這樣子步驟,難道是哪里存在問題嘛,好奇的我,打開了Sources面板,然后就發(fā)現(xiàn)了:
未優(yōu)化的動畫
它的源碼優(yōu)化動畫,使用的是rAF,了解過的人一定不會陌生,你可以簡單的理解就是:按幀對網頁進行重繪。這里就引出了幀的概念,后續(xù)會說明。
rAF的詳細介紹,后續(xù)會對它進行梳理,可以持續(xù)關注。
在將幀的概念前,我們得從背景開始看起,也就是渲染頁面的這個過程,有哪些關鍵性的路徑呢。
五大關鍵渲染路徑
像素輸出到頁面,肯定經歷了很多的過程,那我們作為前端工程師,工作中需要注意的點是哪些呢,這里給出參考:
渲染關鍵路徑
這五個主要的部分,應該是我們值得去關注的,因為我們擁有最大控制權的部分。至于每一個過程具體是怎么樣的呢,不清楚的可以參考下圖:
每個階段任務
所以在這么一個像素的管道里,每部分都有可能造成卡頓,所以我們需要額外的關注這些,畢竟那一部分不當,都會開了不必要的性能開銷。
三種輸出方式
當時我的疑問是: 難道每一幀都總是會經過管道每個部分的處理嘛,其實不是這樣子的,從視覺的角度來說,管道針對指定幀的運行通常有三種方式:
指定幀的運行通常有三種方式
如果我們以第三種方式來更新視圖,也就是更改一個既不要布局也不要繪制的屬性,則瀏覽器將跳到只執(zhí)行合成。
跑個demo
為了更加具體的驗證上述的過程,可以動手跑一個demo,來驗證一下。
demo地址:https://googlechrome.github.io/devtools-samples/jank/
主線程
我們添加多個dom元素進行動畫,效果更佳明顯,接著我們打開Performance,Record這個過程,我們需要關注的是Main選項卡,也就是主線程,我們在放大里面的Task,就有了下圖:
詳細的Task
經歷的過程,也是很清楚看到,Update Layer Tree -->> Layout -->> Paint -->> Composite Layers。
如果你不是很清楚Performance中名稱的含義,可以參考下面這篇文章,點這里:
https://mp.weixin.qq.com/s/iodsGPWgYc97yWLb09Xk6A
接著,我們按下,Optimize按鈕,按照之前的流程走,Record后,發(fā)現(xiàn)不對勁,還是這樣子步驟,難道是哪里存在問題嘛,好奇的我,打開了Sources面板,然后就發(fā)現(xiàn)了:
未優(yōu)化的動畫
它的源碼優(yōu)化動畫,使用的是rAF,了解過的人一定不會陌生,你可以簡單的理解就是:按幀對網頁進行重繪。這里就引出了幀的概念,后續(xù)會說明。
rAF的詳細介紹,后續(xù)會對它進行梳理,可以持續(xù)關注。
如何避免回流與重繪
回到前面我們設想的點,我們如何才能保證直接跳到合成過程,避免Layout以及Paint呢,當然有,我們需要對app.js中的uppdate函數(shù)進行改造,使用transform: translateX(0px); 做動畫,做完update函數(shù)的處理邏輯后,我們再次Record一下:
優(yōu)化后的動畫
從Task子任務中,我們可以發(fā)現(xiàn),Layout -->> Paint, 布局和繪制的過程跳過了。這也是為什么我們常說的需要避免回流與重繪。從主線程上來看,能夠完全的避免這些過程,避免了很多的運算開銷。
這也是為什么經常可以看到這樣子的建議:
- 堅持使用 transform 和 opacity 屬性更改來實現(xiàn)動畫。
- 使用 will-change 或 translateZ 提升移動的元素。
至于使用will-change和translatez來提升圖層,這又是另外知識點了,這里就不張開了。
介紹到這里,我們已經清楚的明白,避免回流和重繪的意義,那么我們提到的幀和rAF 與 渲染路徑有啥關系呢。
如何避免回流與重繪
回到前面我們設想的點,我們如何才能保證直接跳到合成過程,避免Layout以及Paint呢,當然有,我們需要對app.js中的uppdate函數(shù)進行改造,使用transform: translateX(0px); 做動畫,做完update函數(shù)的處理邏輯后,我們再次Record一下:
優(yōu)化后的動畫
從Task子任務中,我們可以發(fā)現(xiàn),Layout -->> Paint, 布局和繪制的過程跳過了。這也是為什么我們常說的需要避免回流與重繪。從主線程上來看,能夠完全的避免這些過程,避免了很多的運算開銷。
這也是為什么經常可以看到這樣子的建議:
- 堅持使用 transform 和 opacity 屬性更改來實現(xiàn)動畫。
- 使用 will-change 或 translateZ 提升移動的元素。
至于使用will-change和translatez來提升圖層,這又是另外知識點了,這里就不張開了。
介紹到這里,我們已經清楚的明白,避免回流和重繪的意義,那么我們提到的幀和rAF 與 渲染路徑有啥關系呢。
幀
我做的第一件事情就是google,然后維基百科給出如下定義:
在視頻領域,電影、電視、數(shù)字視頻等可視為隨時間連續(xù)變換的許多張畫面,其中幀是指每一張畫面。
嗯,不是很好理解,知道我找到了這張圖,才解答了我的困惑:
frame
這就真的是一圖勝千言。
這個圖,你可以理解成就是像素放到屏幕的完整過程。你肯定對里面的一些關鍵信息很迷惑,這里作出一些解釋。
接下來大部分內容都是翻譯的,沒有更多的總結,感興趣可以看看原文。
PROCESSES(進程)
映入眼簾的就是進程:
- Renderer Process: 渲染進程。
- 一個標簽的周圍容器。
- 它包含了多個線程,這些線程共同負責讓你的頁面出現(xiàn)在屏幕上的各個環(huán)節(jié)。
- 這些線程是合成線程(Compositor)、圖塊柵格化線程(Tile Worker)和主線程。
- GPU Process: GPU進程。
- 這是服務于所有標簽和周圍瀏覽器進程的單一進程。
- 當幀被提交時,GPU進程將上傳任何磁貼和其他數(shù)據(jù)(如四維頂點和矩陣)到GPU,以便實際將像素推送到屏幕上。
- GPU進程包含一個單一的線程,稱為GPU線程,實際完成工作。
RENDERER PROCESS THREADS(渲染進程中的線程)
現(xiàn)在我們來看看Renderer Process中的線程。
- Compositor Thread(合成線程):
- 這是第一個被告知vsync事件的線程(這是操作系統(tǒng)告訴瀏覽器制作新幀的方式)。
- 它還將接收任何輸入事件。
- 如果可以的話,合成器線程將避免進入主線程,并將嘗試將輸入(比如說--滾動甩動)轉換為屏幕上的運動。它將通過更新圖層位置并通過GPU線程直接將幀提交給GPU來實現(xiàn)。
- 如果因為輸入事件處理程序或其他視覺工作而無法做到這一點,那么就需要使用主線程。
- Main Thread(主線程):
- 這是瀏覽器執(zhí)行我們都知道和喜歡的任務的地方。JavaScript、樣式、布局和繪畫。(在未來的Houdini中,這種情況會有所改變,我們將能夠在Compositor線程中運行一些代碼。)
- 這個線程贏得了 "最有可能導致jank "的獎項,主要是因為這里有很多東西在運行。(jank值得是頁面抖動)
- Compositor Tile Worker(s) (合成圖塊柵格化線程):
- 由合成線程派生的一個或多個線程,用于處理柵格化任務。我們稍后再討論。
在很多方面,你應該把Compositor線程視為 "大老板"。雖然它不運行JavaScript、Layout、Paint或其他任何東西,但它是完全負責啟動主線程工作的線程,然后將幀運送到屏幕上。如果它不需要等待輸入事件處理程序,它就可以在等待主線程完成工作的同時運送幀。
你也可以想象Service Workers和Web Workers生活在這個過程中,不過我沒有把他們包括在內,因為這讓事情變得更加復雜。
THE FLOW OF THINGS(主線程流程)
讓我們成主線程開始吧。
主線程
讓我們一步步走過這個流程,從vsync到像素,并談談在事件的 "全胖 "版本中事情是如何進行的。值得記住的是,瀏覽器不需要執(zhí)行所有這些步驟,這取決于什么是必要的。例如,如果沒有新的HTML要解析,那么解析HTML就不會啟動。事實上,很多時候,提高性能的最好方法就是簡單地消除部分流程被啟動的必要性!
同樣值得注意的是,樣式和布局下的紅色箭頭似乎指向了 requestAnimationFrame。在你的代碼中完全有可能意外地觸發(fā)這兩者。這就是所謂的強制同步布局(或樣式,視情況而定),它通常對性能不利。
Frame Start(開始新的一幀):
垂直同步信號觸發(fā),開始渲染新的一幀圖像。
Input event handlers (輸入事件的處理)。
-輸入數(shù)據(jù)從合成器線程傳遞給主線程上的任何輸入事件處理程序。
所有的輸入事件處理程序(觸摸移動、滾動、點擊)都應該首先啟動,每幀一次,但情況不一定如此。
調度器會做出最大努力的嘗試,其成功率在不同的操作系統(tǒng)中有所不同。在用戶交互和事件進入主線程處理之間也有一些延遲。
requestAnimationFrame:
這是對屏幕進行視覺更新的理想位置,因為你有新鮮的輸入數(shù)據(jù),而且這是你要得到的最接近vsync的地方。
其他的視覺任務,比如樣式計算,都是在這個任務之后進行的,所以它的理想位置是突變元素。
如果你突變--比如說--100個類,這不會導致100個樣式計算;它們將被分批處理,并在后面處理。唯一需要注意的是,你不要查詢任何計算過的樣式或布局屬性(比如el.style.backgroundImage或el.style.offsetWidth)。
如果你這樣做,你會把重新計算的樣式、布局或兩者都向前帶,導致強制的同步布局,或者更糟糕的是,布局打亂。
Parse HTML (解析 HTML):
任何新添加的HTML都會被處理,并創(chuàng)建DOM元素。
在頁面加載過程中或appendChild等操作后,你可能會看到更多的這種情況。
Recalc Styles(重新計算樣式):
樣式是為任何新添加或突變的東西計算的,這可能是整個樹,也可能是范圍,這取決于改變了什么。
這可能是整個樹,也可能是范圍縮小,這取決于改變了什么。
例如,改變主體上的類可能影響深遠,但值得注意的是,瀏覽器已經非常聰明地自動限制了樣式計算的范圍。
Layout(繪制):
計算每個可見元素的幾何信息(每個元素的位置和大小)。它通常對整個文檔進行計算,通常使計算成本與DOM大小成正比。
Update Layer Tree(更新圖層樹):
創(chuàng)建疊加上下文和深度排序元素的過程。
Paint:
這是兩部分過程中的第一部分:繪制是記錄任何新元素或視覺上有變化的元素的繪制調用(在這里填充一個矩形,在那里寫文字)。
第二部分是光柵化(見下文),在這里執(zhí)行繪圖調用,并填充紋理。這一部分是對繪制調用的記錄,通常比光柵化快得多,但這兩部分通常統(tǒng)稱為 "繪畫"。
Composite(合成):
計算出圖層和瓷磚的信息,并傳回給合成器線程來處理。
這將會考慮到,除其他事項外,像will-change,重疊元素,以及任何硬件加速的canvases。
Raster Scheduled (柵格化規(guī)劃)and Rasterize(柵格化):
現(xiàn)在會執(zhí)行在Paint任務中記錄的繪制調用。這是在Compositor Tile Workers中完成的,其數(shù)量取決于平臺和設備能力。
例如,在Android上,你通常會發(fā)現(xiàn)一個Worker,在桌面上,你有時可以找到四個。柵格化是以圖層為單位進行的,每個圖層都是由瓷磚組成的。
Frame End(幀結束):
當各個圖層的磁貼都柵格化后,任何新的磁貼都會和輸入數(shù)據(jù)(可能在事件處理程序中被改變)一起提交給GPU線程。
Frame Ships(發(fā)送幀):
最后,但絕不是最不重要的,磁貼由GPU線程上傳至GPU。GPU使用四邊形和矩陣(所有常見的GL好東西)將磁貼繪制到屏幕上。
大致上,整個的過程就是上述。
我做的第一件事情就是google,然后維基百科給出如下定義:
在視頻領域,電影、電視、數(shù)字視頻等可視為隨時間連續(xù)變換的許多張畫面,其中幀是指每一張畫面。
嗯,不是很好理解,知道我找到了這張圖,才解答了我的困惑:
frame
這就真的是一圖勝千言。
這個圖,你可以理解成就是像素放到屏幕的完整過程。你肯定對里面的一些關鍵信息很迷惑,這里作出一些解釋。
接下來大部分內容都是翻譯的,沒有更多的總結,感興趣可以看看原文。
PROCESSES(進程)
映入眼簾的就是進程:
- Renderer Process: 渲染進程。
- 一個標簽的周圍容器。
- 它包含了多個線程,這些線程共同負責讓你的頁面出現(xiàn)在屏幕上的各個環(huán)節(jié)。
- 這些線程是合成線程(Compositor)、圖塊柵格化線程(Tile Worker)和主線程。
- GPU Process: GPU進程。
- 這是服務于所有標簽和周圍瀏覽器進程的單一進程。
- 當幀被提交時,GPU進程將上傳任何磁貼和其他數(shù)據(jù)(如四維頂點和矩陣)到GPU,以便實際將像素推送到屏幕上。
- GPU進程包含一個單一的線程,稱為GPU線程,實際完成工作。
RENDERER PROCESS THREADS(渲染進程中的線程)
現(xiàn)在我們來看看Renderer Process中的線程。
- Compositor Thread(合成線程):
- 這是第一個被告知vsync事件的線程(這是操作系統(tǒng)告訴瀏覽器制作新幀的方式)。
- 它還將接收任何輸入事件。
- 如果可以的話,合成器線程將避免進入主線程,并將嘗試將輸入(比如說--滾動甩動)轉換為屏幕上的運動。它將通過更新圖層位置并通過GPU線程直接將幀提交給GPU來實現(xiàn)。
- 如果因為輸入事件處理程序或其他視覺工作而無法做到這一點,那么就需要使用主線程。
- Main Thread(主線程):
- 這是瀏覽器執(zhí)行我們都知道和喜歡的任務的地方。JavaScript、樣式、布局和繪畫。(在未來的Houdini中,這種情況會有所改變,我們將能夠在Compositor線程中運行一些代碼。)
- 這個線程贏得了 "最有可能導致jank "的獎項,主要是因為這里有很多東西在運行。(jank值得是頁面抖動)
- Compositor Tile Worker(s) (合成圖塊柵格化線程):
- 由合成線程派生的一個或多個線程,用于處理柵格化任務。我們稍后再討論。
在很多方面,你應該把Compositor線程視為 "大老板"。雖然它不運行JavaScript、Layout、Paint或其他任何東西,但它是完全負責啟動主線程工作的線程,然后將幀運送到屏幕上。如果它不需要等待輸入事件處理程序,它就可以在等待主線程完成工作的同時運送幀。
你也可以想象Service Workers和Web Workers生活在這個過程中,不過我沒有把他們包括在內,因為這讓事情變得更加復雜。
THE FLOW OF THINGS(主線程流程)
讓我們成主線程開始吧。
主線程
讓我們一步步走過這個流程,從vsync到像素,并談談在事件的 "全胖 "版本中事情是如何進行的。值得記住的是,瀏覽器不需要執(zhí)行所有這些步驟,這取決于什么是必要的。例如,如果沒有新的HTML要解析,那么解析HTML就不會啟動。事實上,很多時候,提高性能的最好方法就是簡單地消除部分流程被啟動的必要性!
同樣值得注意的是,樣式和布局下的紅色箭頭似乎指向了 requestAnimationFrame。在你的代碼中完全有可能意外地觸發(fā)這兩者。這就是所謂的強制同步布局(或樣式,視情況而定),它通常對性能不利。
Frame Start(開始新的一幀):
垂直同步信號觸發(fā),開始渲染新的一幀圖像。
Input event handlers (輸入事件的處理)。
-輸入數(shù)據(jù)從合成器線程傳遞給主線程上的任何輸入事件處理程序。
所有的輸入事件處理程序(觸摸移動、滾動、點擊)都應該首先啟動,每幀一次,但情況不一定如此。
調度器會做出最大努力的嘗試,其成功率在不同的操作系統(tǒng)中有所不同。在用戶交互和事件進入主線程處理之間也有一些延遲。
requestAnimationFrame:
這是對屏幕進行視覺更新的理想位置,因為你有新鮮的輸入數(shù)據(jù),而且這是你要得到的最接近vsync的地方。
其他的視覺任務,比如樣式計算,都是在這個任務之后進行的,所以它的理想位置是突變元素。
如果你突變--比如說--100個類,這不會導致100個樣式計算;它們將被分批處理,并在后面處理。唯一需要注意的是,你不要查詢任何計算過的樣式或布局屬性(比如el.style.backgroundImage或el.style.offsetWidth)。
如果你這樣做,你會把重新計算的樣式、布局或兩者都向前帶,導致強制的同步布局,或者更糟糕的是,布局打亂。
Parse HTML (解析 HTML):
任何新添加的HTML都會被處理,并創(chuàng)建DOM元素。
在頁面加載過程中或appendChild等操作后,你可能會看到更多的這種情況。
Recalc Styles(重新計算樣式):
樣式是為任何新添加或突變的東西計算的,這可能是整個樹,也可能是范圍,這取決于改變了什么。
這可能是整個樹,也可能是范圍縮小,這取決于改變了什么。
例如,改變主體上的類可能影響深遠,但值得注意的是,瀏覽器已經非常聰明地自動限制了樣式計算的范圍。
Layout(繪制):
計算每個可見元素的幾何信息(每個元素的位置和大小)。它通常對整個文檔進行計算,通常使計算成本與DOM大小成正比。
Update Layer Tree(更新圖層樹):
創(chuàng)建疊加上下文和深度排序元素的過程。
Paint:
這是兩部分過程中的第一部分:繪制是記錄任何新元素或視覺上有變化的元素的繪制調用(在這里填充一個矩形,在那里寫文字)。
第二部分是光柵化(見下文),在這里執(zhí)行繪圖調用,并填充紋理。這一部分是對繪制調用的記錄,通常比光柵化快得多,但這兩部分通常統(tǒng)稱為 "繪畫"。
Composite(合成):
計算出圖層和瓷磚的信息,并傳回給合成器線程來處理。
這將會考慮到,除其他事項外,像will-change,重疊元素,以及任何硬件加速的canvases。
Raster Scheduled (柵格化規(guī)劃)and Rasterize(柵格化):
現(xiàn)在會執(zhí)行在Paint任務中記錄的繪制調用。這是在Compositor Tile Workers中完成的,其數(shù)量取決于平臺和設備能力。
例如,在Android上,你通常會發(fā)現(xiàn)一個Worker,在桌面上,你有時可以找到四個。柵格化是以圖層為單位進行的,每個圖層都是由瓷磚組成的。
Frame End(幀結束):
當各個圖層的磁貼都柵格化后,任何新的磁貼都會和輸入數(shù)據(jù)(可能在事件處理程序中被改變)一起提交給GPU線程。
Frame Ships(發(fā)送幀):
最后,但絕不是最不重要的,磁貼由GPU線程上傳至GPU。GPU使用四邊形和矩陣(所有常見的GL好東西)將磁貼繪制到屏幕上。
大致上,整個的過程就是上述。
requestIdleCallback
要說這個的話,我們得拿requestAnimationFrame來類比,requestAnimationFrame是在重新渲染屏幕之前執(zhí)行的,上面提到的rAF,當時做的就是優(yōu)化動畫,所以很適合做動畫。
requestIdleCallback你通過主線程里面中的Task去查找的話,會發(fā)現(xiàn)它是在渲染屏幕之后執(zhí)行,通過查閱文章發(fā)現(xiàn),一般會看瀏覽器是否空閑。
【責任編輯:武曉燕 TEL:(010)68476606】
要說這個的話,我們得拿requestAnimationFrame來類比,requestAnimationFrame是在重新渲染屏幕之前執(zhí)行的,上面提到的rAF,當時做的就是優(yōu)化動畫,所以很適合做動畫。
requestIdleCallback你通過主線程里面中的Task去查找的話,會發(fā)現(xiàn)它是在渲染屏幕之后執(zhí)行,通過查閱文章發(fā)現(xiàn),一般會看瀏覽器是否空閑。