「前端性能」避免迴流和重繪的必要性

前言

本文會介紹瀏覽器中幀(Frame)的概念,它的流程是怎麼樣的。前端

至於寫這個文章的出發點在於,我好奇瀏覽器中像素工做流程是怎麼樣的,何時開始的,最後的結果是什麼。git

基於這些好奇,查閱了些外文資料,本文提供了些參考,參考連接在文末。github

最近搞了性能優化思惟導圖,還在持續輸出中,點擊這裏web

原因

在講幀的概念前,咱們得從背景開始看起,也就是渲染頁面的這個過程,有哪些關鍵性的路徑呢。chrome

五大關鍵渲染路徑

像素輸出到頁面,確定經歷了不少的過程,那咱們做爲前端工程師,工做中須要注意的點是哪些呢,這裏給出參考:canvas

渲染關鍵路徑

這五個主要的部分,應該是咱們值得去關注的,由於咱們擁有最大控制權的部分。至於每個過程具體是怎麼樣的呢,不清楚的能夠參考下圖:瀏覽器

詳細的工做

因此在這麼一個像素的管道里,每部分都有可能形成卡頓,因此咱們須要額外的關注這些,畢竟那一部分不當,都會開了沒必要要的性能開銷。性能優化

三種輸出方式

當時個人疑問是: 難道每一幀都老是會通過管道每一個部分的處理嘛,其實不是這樣子的,從視覺的角度來講,管道針對指定幀的運行一般有三種方式:markdown

指定幀的運行一般有三種方式

若是咱們以第三種方式來更新視圖,也就是更改一個既不要佈局也不要繪製的屬性,則瀏覽器將跳到只執行合成。前端工程師

跑個demo

爲了更加具體的驗證上述的過程,能夠動手跑一個demo,來驗證一下。

demo地址:googlechrome.github.io/devtools-sa…

主線程-火焰圖

咱們添加多個dom元素進行動畫,效果更佳明顯,接着咱們打開Performance,Record這個過程,咱們須要關注的是Main選項卡,也就是主線程,咱們在放大里面的Task,就有了下圖:

主線程-火焰圖-2

經歷的過程,也是很清楚看到,Update Layer Tree -->> Layout -->> Paint -->> Composite Layers。

若是你不是很清楚Performance中名稱的含義,能夠參考下面這篇文章,點這裏:

mp.weixin.qq.com/s/iodsGPWgY…

接着,咱們按下,Optimize按鈕,按照以前的流程走,Record後,發現不對勁,仍是這樣子步驟,難道是哪裏存在問題嘛,好奇的我,打開了Sources面板,而後就發現了:

優化後的動畫

它的源碼優化動畫,使用的是rAF,瞭解過的人必定不會陌生,你能夠簡單的理解就是:按幀對網頁進行重繪。這裏就引出了幀的概念,後續會說明。

rAF的詳細介紹,後續會對它進行梳理,能夠持續關注。

如何避免迴流與重繪

回到前面咱們設想的點,咱們如何才能保證直接跳到合成過程,避免Layout以及Paint呢,固然有,咱們須要對app.js中的uppdate函數進行改造,使用transform: translateX(0px); 作動畫,作完update函數的處理邏輯後,咱們再次Record一下:

優化後的動畫

從Task子任務中,咱們能夠發現,Layout -->> Paint, 佈局和繪製的過程跳過了。這也是爲何咱們常說的須要避免迴流與重繪。從主線程上來看,可以徹底的避免這些過程,避免了不少的運算開銷。

這也是爲何常常能夠看到這樣子的建議:

  • 堅持使用 transform 和 opacity 屬性更改來實現動畫。
  • 使用 will-changetranslateZ 提高移動的元素。

至於使用will-change和translatez來提高圖層,這又是另外知識點了,這裏就不張開了。

介紹到這裏,咱們已經清楚的明白,避免迴流和重繪的意義,那麼咱們提到的rAF 與 渲染路徑有啥關係呢。

我作的第一件事情就是google,而後維基百科給出以下定義:

視頻領域,電影電視數字視頻等可視爲隨時間連續變換的許多張畫面,其中是指每一張畫面。

嗯,不是很好理解,知道我找到了這張圖,才解答了個人困惑:

anatomy-of-a-frame.jpg

這就真的是一圖勝千言

這個圖,你能夠理解成就是像素放到屏幕的完整過程。你確定對裏面的一些關鍵信息很迷惑,這裏做出一些解釋。

接下來大部份內容都是翻譯的,沒有更多的總結,感興趣能夠看看原文。

PROCESSES(進程)

映入眼簾的就是進程:

  • Renderer Process: 渲染進程。
    • 一個標籤的周圍容器。
    • 它包含了多個線程,這些線程共同負責讓你的頁面出如今屏幕上的各個環節。
    • 這些線程是合成線程(Compositor)、圖塊柵格化線程(Tile Worker)和主線程。
  • GPU Process: GPU進程。
    • 這是服務於全部標籤和周圍瀏覽器進程的單一進程。
    • 當幀被提交時,GPU進程將上傳任何磁貼和其餘數據(如四維頂點和矩陣)到GPU,以便實際將像素推送到屏幕上。
    • GPU進程包含一個單一的線程,稱爲GPU線程,實際完成工做。

RENDERER PROCESS THREADS(渲染進程中的線程)

如今咱們來看看Renderer Process中的線程。

  • Compositor Thread(合成線程):
    • 這是第一個被告知vsync事件的線程(這是操做系統告訴瀏覽器製做新幀的方式)。
    • 它還將接收任何輸入事件。
    • 若是能夠的話,合成器線程將避免進入主線程,並將嘗試將輸入(好比說--滾動甩動)轉換爲屏幕上的運動。它將經過更新圖層位置並經過GPU線程直接將幀提交給GPU來實現。
    • 若是由於輸入事件處理程序或其餘視覺工做而沒法作到這一點,那麼就須要使用主線程。
  • Main Thread(主線程):
    • 這是瀏覽器執行咱們都知道和喜歡的任務的地方。JavaScript、樣式、佈局和繪畫。(在將來的Houdini中,這種狀況會有所改變,咱們將可以在Compositor線程中運行一些代碼。)
    • 這個線程贏得了 "最有可能致使jank "的獎項,主要是由於這裏有不少東西在運行。(jank值得是頁面抖動)
  • Compositor Tile Worker(s) (合成圖塊柵格化線程):
    • 由合成線程派生的一個或多個線程,用於處理柵格化任務。咱們稍後再討論。

在不少方面,你應該把Compositor線程視爲 "大老闆"。雖然它不運行JavaScript、Layout、Paint或其餘任何東西,但它是徹底負責啓動主線程工做的線程,而後將幀運送到屏幕上。若是它不須要等待輸入事件處理程序,它就能夠在等待主線程完成工做的同時運送幀。

你也能夠想象Service Workers和Web Workers生活在這個過程當中,不過我沒有把他們包括在內,由於這讓事情變得更加複雜。

THE FLOW OF THINGS(主線程流程)

讓咱們成主線程開始吧。

image.png

讓咱們一步步走過這個流程,從vsync到像素,並談談在事件的 "全胖 "版本中事情是如何進行的。值得記住的是,瀏覽器不須要執行全部這些步驟,這取決於什麼是必要的。例如,若是沒有新的HTML要解析,那麼解析HTML就不會啓動。事實上,不少時候,提升性能的最好方法就是簡單地消除部分流程被啓動的必要性!

一樣值得注意的是,樣式和佈局下的紅色箭頭彷佛指向了 requestAnimationFrame。在你的代碼中徹底有可能意外地觸發這二者。這就是所謂的強制同步佈局(或樣式,視狀況而定),它一般對性能不利。

  1. Frame Start開始新的一幀):

    1. 垂直同步信號觸發,開始渲染新的一幀圖像。

  2. Input event handlers輸入事件的處理)。

    1. -輸入數據從合成器線程傳遞給主線程上的任何輸入事件處理程序。

    2. 全部的輸入事件處理程序(觸摸移動、滾動、點擊)都應該首先啓動,每幀一次,但狀況不必定如此。

    3. 調度器會作出最大努力的嘗試,其成功率在不一樣的操做系統中有所不一樣。在用戶交互和事件進入主線程處理之間也有一些延遲。

  3. requestAnimationFrame

    1. 這是對屏幕進行視覺更新的理想位置,由於你有新鮮的輸入數據,並且這是你要獲得的最接近vsync的地方。

    2. 其餘的視覺任務,好比樣式計算,都是在這個任務以後進行的,因此它的理想位置是突變元素。

    3. 若是你突變--好比說--100個類,這不會致使100個樣式計算;它們將被分批處理,並在後面處理。惟一須要注意的是,你不要查詢任何計算過的樣式或佈局屬性(好比el.style.backgroundImage或el.style.offsetWidth)。

    4. 若是你這樣作,你會把從新計算的樣式、佈局或二者都向前帶,致使強制的同步佈局,或者更糟糕的是,佈局打亂。

  4. Parse HTML (解析 HTML):

    1. 任何新添加的HTML都會被處理,並建立DOM元素。

    2. 在頁面加載過程當中或appendChild等操做後,你可能會看到更多的這種狀況。

  5. Recalc Styles從新計算樣式):

    1. 樣式是爲任何新添加或突變的東西計算的,這多是整個樹,也多是範圍,這取決於改變了什麼。

    2. 這多是整個樹,也多是範圍縮小,這取決於改變了什麼。

    3. 例如,改變主體上的類可能影響深遠,但值得注意的是,瀏覽器已經很是聰明地自動限制了樣式計算的範圍。

  6. Layout(繪製):

    1. 計算每一個可見元素的幾何信息(每一個元素的位置和大小)。它一般對整個文檔進行計算,一般使計算成本與DOM大小成正比。

  7. Update Layer Tree更新圖層樹):

    1. 建立疊加上下文和深度排序元素的過程。
  8. Paint:

    1. 這是兩部分過程當中的第一部分:繪製是記錄任何新元素或視覺上有變化的元素的繪製調用(在這裏填充一個矩形,在那裏寫文字)。
    2. 第二部分是光柵化(見下文),在這裏執行繪圖調用,並填充紋理。這一部分是對繪製調用的記錄,一般比光柵化快得多,但這兩部分一般統稱爲 "繪畫"。
  9. Composite合成):

    1. 計算出圖層和瓷磚的信息,並傳回給合成器線程來處理。

    2. 這將會考慮到,除其餘事項外,像will-change,重疊元素,以及任何硬件加速的canvases。

  10. Raster Scheduled柵格化規劃)and Rasterize柵格化):

    1. 如今會執行在Paint任務中記錄的繪製調用。這是在Compositor Tile Workers中完成的,其數量取決於平臺和設備能力。

    2. 例如,在Android上,你一般會發現一個Worker,在桌面上,你有時能夠找到四個。柵格化是以圖層爲單位進行的,每一個圖層都是由瓷磚組成的。

  11. Frame End(幀結束):

    1. 當各個圖層的磁貼都柵格化後,任何新的磁貼都會和輸入數據(可能在事件處理程序中被改變)一塊兒提交給GPU線程。

  12. Frame Ships發送幀):

    1. 最後,但毫不是最不重要的,磁貼由GPU線程上傳至GPU。GPU使用四邊形和矩陣(全部常見的GL好東西)將磁貼繪製到屏幕上。

大體上,整個的過程就是上述。

requestIdleCallback

要說這個的話,咱們得拿requestAnimationFrame來類比,requestAnimationFrame是在從新渲染屏幕以前執行的,上面提到的rAF,當時作的就是優化動畫,因此很適合作動畫。

requestIdleCallback你經過主線程裏面中的Task去查找的話,會發現它是在渲染屏幕以後執行,經過查閱文章發現,通常會看瀏覽器是否空閒。

這裏篇幅有限,想要了解這個的話,推薦一篇文章:

juejin.cn/post/684490…

總結

最近查閱外文文獻,發現要學的東西太多了,若是這篇文章有寫的不對,或者翻譯不佳的地方,歡迎小夥伴指出。

原文首發地址點這裏,歡迎你們關注公衆號「TianTianUp」。

我是TianTian,咱們下一期見!!!

參考

[1] w3c-longTasks: github.com/w3c/longtas…

[2] chrome-fps-meter: developer.chrome.com/docs/devtoo…

[3] devtools-samples: googlechrome.github.io/devtools-sa…

[4] Analyze runtime performance: developer.chrome.com/docs/devtoo…

[5] Timeline Event Reference: developer.chrome.com/docs/devtoo…

[6] The Anatomy of a Frame: aerotwist.com/blog/the-an…

[7] performance-rendering: developers.google.com/web/fundame…

[8] 維基百科: zh.wikipedia.org/wiki/

相關文章
相關標籤/搜索