**首先說翻譯這篇文章的目的實際上是,以前回答的關於瀏覽器js渲染的問題被打臉了 ಥ_ಥ ,
不得不正視本身半路出家學前端的事實,因此這篇文章就算是本身的一個筆記吧,學而時習之,不亦樂乎,翻譯錯了,還請批評指正**javascript
原文連接:Rendering: repaint, reflow/relayout, restylephp
正文:css
今天咱們來談談這個從page2.0(譯者注:page2.0不怎麼常見,應該是做者自創的,介紹連接在文章結尾)生命週期的詞:渲染,有時候它甚至發生在瀑布流當中。
那麼,瀏覽器是如何靠着一大塊html,css,javascript代碼在屏幕上顯示你的頁面的呢?html
不一樣的瀏覽器有不一樣的實現方式,可是下邊的圖會展現一個當代碼被下載到電腦裏以後全部瀏覽器的共同實現(或多或少都有)前端
瀏覽器把咱們的html源代碼解析而且初始化成一個dom樹,dom樹是一個數據結構,它的特色包括,每一個html標籤都在這個樹上有一個對應的節點,固然標籤當中的文本塊也在dom樹上有一個相應的文本節點,這個dom樹的根節點就是documentElement(也就是<html></html>標籤)java
瀏覽器解析css代碼,針對一些像-webkit、-moz以及一些不認識的寫法作忽略,css的優先級是這樣的:最基本的瀏覽器默認樣式,而後就是來自外部引入的用戶腳本,最高級的是在頁面裏邊寫在<style>標籤裏面的樣式node
接下來就是有意思的部分:初始化一個渲染樹。渲染師有點相似於dom樹,可是並非徹底相同的。渲染樹知道樣式(style),因此若是你用display:none來隱藏一個div,那麼這個div並不會在渲染樹上被引入,其餘的一些不可見的好比head也是同樣的道理。另外一方面,一個dom節點在渲染樹上可能對應多個節點,好比說文本節點,<p>標籤每一行都須要一個渲染節點。在渲染樹上的一個節點被稱爲一個frame,或者是一個box(跟css box相似),每一個渲染樹節點都有css box屬性-width、height、border、margin以及其餘。web
一旦渲染樹初始化完成,瀏覽器就能夠在屏幕上開始繪製節點。ajax
舉個例子:瀏覽器
<html> <head> <title>Beautiful page</title> </head> <body> <p> Once upon a time there was a looong paragraph... </p> <div style="display: none"> Secret message </div> <div><img src="..." /></div> ... </body> </html>
基本上來講,dom樹映射html源碼的方式是一個節點對應一個標籤,一個文本節點對應一個標籤裏的文字(簡單起見忽略空白也對應文本節點的事實)
documentElement (html) head title body p [text node] div [text node] div img ...
渲染樹會成爲dom樹的可見部分,它拋棄了一些東西-head裏的以及hidden起來的div,可是它會爲文本的每一行產生另外的節點(aka frames, aka boxes)。
root (RenderView) body p line 1 line 2 line 3 ... div img ...
渲染樹的根節點會包括全部的其餘節點,因此你能夠把根節點當成瀏覽器窗口的區域,頁面被限制在這裏,技術上webkit把根節點稱做renderView而且對應css當中的initial containing block(初始化包含塊),也就是從page(0,0)到(window.width,window.height)的矩形顯示區。
接下來遞歸遍歷整個渲染樹來了解什麼東西以及如何讓這些顯示在屏幕上
無論何時至少會有一個初始化頁面佈局和一個繪製行爲(原文:There's always at least one initial page layout together with a paint )(除非你的頁面是空白的),以後,若是改變構造渲染樹信息都會觸發下邊一種或兩種結果:
渲染樹的一部分或者整棵樹都會被從新分析而且節點尺寸被從新計算,這就被稱爲reflow或者是layout又或者是layouting。注意至少會有一個reflow,即頁面的初始化佈局。
屏幕的部分區域須要更新,有多是節點的形狀變化也有多是樣式變了好比背景顏色。屏幕的變化就被稱爲repaint或者是redraw。
不論是reflow仍是repaint都是很耗費資源的事,它們會損害用戶體驗以及讓ui顯得卡頓
任何改變用於構建渲染樹的初始化信息的都會觸發reflow或者repaint,好比:
添加、刪除、改變dom節點
經過display: none隱藏一個節點(觸發reflow and repaint)以及經過visibility: hidden隱藏節點(僅觸發repaint)
移動或者給一個節點添加動畫
添加一個樣式,或者調整樣式
用戶操做好比調整窗口大小,改變字體大小,或者滾動等
看個例子:
var bstyle = document.body.style; // cache bstyle.padding = "20px"; // reflow, repaint bstyle.border = "10px solid red"; // another reflow and a repaint bstyle.color = "blue"; // repaint only, no dimensions changed bstyle.backgroundColor = "#fad"; // repaint bstyle.fontSize = "2em"; // reflow, repaint // new DOM element - reflow, repaint document.body.appendChild(document.createTextNode('dude!'));
有些reflow會有更大的開銷,試想一個渲染樹,若是有一個body下的直接子節點,對他胡亂操做可能並不會影響其餘的節點,可是當你操做一個頂部的div改變他的尺寸或者加動畫,以此來推進其餘部分,這聽起來就很費資源
reflow、repaint跟渲染樹的聯繫起來使得開銷變大。而瀏覽器的目標之一就是減小reflow以及repaint的負面影響,其中的一個策略就是乾脆不作,又或者說至少不是如今作。瀏覽器會設置一個隊列來收集這些要改變渲染樹或屏幕的動做,而且分批執行,這樣每個須要一系列reflow的變化會被整合在一塊兒,因此最終只有一個reflow須要被計算分析。瀏覽器能夠把這些變化添加在隊列當中而後一旦到達定時器時間或者必定數量的操做就開始(刷新)執行。
但有時腳本語句會破化瀏覽器優化reflow,並使其刷新隊列以及執行全部批處理的改變。這些發生在你請求樣式信息,好比:
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
getComputedStyle(), 或者在IE當中的currentStyle
全部這些上述的都是關於一個節點基本的請求樣式信息,無論何時請求,瀏覽器只能給你一個最精確的值,所以瀏覽器須要將隊列中的全部行爲所有執行完畢,而且強制reflow.
好比說,一句話處理set和get樣式信息就不是個好的實踐
// 不要這麼作 el.style.left = el.offsetLeft + 10 + "px";
減小reflows以及repaint對用戶體驗的負面影響根本上來講是減小reflow以及repaint的次數以及減小對樣式信息的請求,這樣瀏覽器就能夠優化reflows,可是如何去作呢?
不要試圖逐個的的改變初始樣式,對靜態頁面來講,明智而且可維護的作法是改變class的名字,而不是一個接一個改樣式。對動態的樣式來講,修改csstext要比直接接觸元素修改樣式要好。
// 不要這麼作 var left = 10, top = 10; el.style.left = left + "px"; el.style.top = top + "px"; // 好的作法 el.className += " theclassname"; // or when top and left are calculated dynamically... // 好的作法 el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
離線批量處理dom改變,離線意味着不要在當前的dom樹中操做,你能夠:
1 經過documentFragment處理臨時操做
2 把你即將進行操做的節點複製出來,操做副本,而後替換即將要操做的節點
3 用display:none把節點藏起來(一次reflow,一次repaint),添加完大量的操做,從新展現(一次reflow,一次repaint),用這種方式你能夠只渲染兩次,大大減小渲染次數
不要頻繁的訪問樣式計算,若是有須要對一個樣式進行屢次計算,只作一次,把它緩存到一個變量當中,對本地這個變量進行操做,下邊看一個實踐:
// 別這麼作 for(big; loop; here) { el.style.left = el.offsetLeft + 10 + "px"; el.style.top = el.offsetTop + 10 + "px"; } // 好的作法 var left = el.offsetLeft, top = el.offsetTop esty = el.style; for(big; loop; here) { left += 10; top += 10; esty.left = left + "px"; esty.top = top + "px"; }
總之,當你作完操做以後請想一想渲染樹以及耗費的資源,好比說使用絕對定位的元素,若是你對它作動畫操做不會影響其餘太多的元素,當你把這個節點放在其餘節點的最上面的時候,這個時候這個區域只須要repaint,而不須要reflow
讓咱們簡單的用一個工具來看看restyle(不影響幾何形狀的渲染樹的變化)、reflow(影響佈局)以及repaint的區別:
咱們首先比較一個作一件事的兩個方式,第一種方式,咱們改變一些樣式(跟佈局無關的),每一步作完以後,咱們檢查一次屬性變化,每步變化之間沒有聯繫
bodystyle.color = 'red'; tmp = computed.backgroundColor; bodystyle.color = 'white'; tmp = computed.backgroundImage; bodystyle.color = 'green'; tmp = computed.backgroundAttachment;
第二種方式,咱們再改變完以後再獲取style的屬性
bodystyle.color = 'yellow'; bodystyle.color = 'pink'; bodystyle.color = 'blue'; tmp = computed.backgroundColor; tmp = computed.backgroundImage; tmp = computed.backgroundAttachment;
上邊兩種方式使用的變量定義以下:
var bodystyle = document.body.style; var computed; if (document.body.currentStyle) { computed = document.body.currentStyle; } else { computed = document.defaultView.getComputedStyle(document.body, ''); }
上面兩中方法的樣式改變會被click以後執行。測試頁面-restyle.html(點擊「dude」)。咱們就叫它restyle test
第二個測試跟第一個同樣,可是咱們同時會改變佈局信息:
// 改一步檢查一步 bodystyle.color = 'red'; bodystyle.padding = '1px'; tmp = computed.backgroundColor; bodystyle.color = 'white'; bodystyle.padding = '2px'; tmp = computed.backgroundImage; bodystyle.color = 'green'; bodystyle.padding = '3px'; tmp = computed.backgroundAttachment; // 作完再檢查 bodystyle.color = 'yellow'; bodystyle.padding = '4px'; bodystyle.color = 'pink'; bodystyle.padding = '5px'; bodystyle.color = 'blue'; bodystyle.padding = '6px'; tmp = computed.backgroundColor; tmp = computed.backgroundImage; tmp = computed.backgroundAttachment;
第二個測試咱們就叫它「relayout test」,source
經過DynaTrace工具咱們看到restyle的可視化信息:
頁面加載完以後,我點擊了一次來執行第一方案(改完以後馬上檢查,在大約第二秒處),而後點擊第二種方案(所有改完以後才檢查,大約四秒處)
工具顯示了頁面加載的方式而且能夠看到那個IElogo顯示正在加載,而後將鼠標光標在下面rendering以顯示事件。滾輪放大看更多細節:
咱們看到藍色的javascript條以及綠色的rendering條,咱們注意到條的長度,渲染比js執行要耗費更多的時間,在ajax及其複雜應用當中,js行爲不是應用瓶頸,dom的訪問、操縱、執行纔是。
好如今咱們來跑一下relayout test,改變body幾何形狀,這一次看看"PurePaths"這個面板,查看每一項的執行時間線,下圖中高亮部分顯示的是第一次點擊事件,執行一段JavaScript邏輯實現一些layout操做。
再次,放大到了有趣的部分,你能夠看到,如今除了「繪製」吧,還有一個新的 - 在「計算流佈局」,由於在這個測試中,咱們除了repaint以外也reflow了
如今,讓咱們來測試在同一頁在Chrome中,並期待在SpeedTracer結果。
這是第一個「restyle」試放大到了有趣的部分,看看發生了啥。
整體上就是點擊一次繪製一次,可是在第一次點擊上,有50%的時間花在從新計算樣式。其實這是由於每次樣式改變都須要詢問樣式信息
放大事件並顯示隱藏線(灰線是由Speedtracer隱藏,由於他們很快),咱們能夠清楚地看到發生了什麼事 - 第一次點擊後,樣式計算三次。第二次以後 - 只有一次計算。
如今咱們來看看relayout test,事件的整個列表看起來是同樣的:
可是詳細視圖顯示了第一次點擊致使了三次reflow(由於須要詢問樣式信息),第二次只致使了一次reflow,如今咱們知道是怎麼回事了
上述兩種工具的區別在於:DynaTrace會顯示layout行爲被執行和加入執行隊列的詳細時間,而SpeedTracer不會;SpeedTracer會將restyle與reflow/layout兩種瀏覽器行爲區別開,而DynaTrace不會。難道IE瀏覽器自己不會區分這兩種行爲?另外,在兩種不一樣的邏輯測試-改變-最後檢查(change-end-touch)與改變-當即檢查(change-then-touch)中,DynaTrace並不會顯示二者觸發迴流的次數不一樣(第一種之觸發一次,第二次觸發3次,而DynaTrace統一顯示爲一次),難道IE瀏覽器的工做機制本就如此?
即便運行上述測試幾百次,IE瀏覽器仍然不關心你在改變樣式後是否請求樣式信息
在屢次運行上述測試後,獲得幾點結論以下:
Chrome中,相比較改變樣式後當即檢查樣式信息,等待所有樣式修改完畢後統一檢查,在restyle測試中會快2.5倍,relayout測試中快4.42倍;
Firefox中,restyle測試快1.87倍,relayout測試快4.64倍;
IE6和IE8,無所謂了
在全部的瀏覽器當中改變樣式只花費同時改變佈局和樣式一半的開銷,除了IE,IE當中改變佈局要比只改變樣式多四倍的花銷
謝謝你看完這個長長的帖子,但願你們注意這些reflows,做爲總結,我再次解釋一下術語:
渲染樹 - DOM樹的可見部分
渲染樹上的節點被稱爲frame或者boxes
渲染樹的從新計算被稱爲reflow(火狐當中),在其餘瀏覽器被稱爲relayout
將從新計算後的渲染樹更新到屏幕的行爲叫作repaint,或者redraw(IE當中)
譯者的一堆問題(之後補充,譯者懶癌發做了):
page2.0介紹