FROM ME:javascript
重繪重排重渲染是瀏覽器呈現頁面時須要經歷的過程,處理得好能夠提高頁面的性能,帶來更好的用戶體驗php
這篇文章也針對重繪重排重渲染從頭至尾作一個比較詳細的說明:css
瀏覽器渲染頁面的過程:渲染樹相似DOM樹,可是並非一一對應的。html
重繪與重排:重排和重繪代價是高昂的,它們會破壞用戶體驗,而且讓UI展現很是遲緩。java
哪些狀況下會觸發重繪與重排;node
瀏覽器針對重繪與重排有哪些策略:瀏覽器會基於你的腳本要求建立一個變化的隊列,而後分批去展示。可是有時你的代碼會組織瀏覽器優化重排並當即刷新隊列,與此同時展現全部批次的變化。web
經過減小重排/重繪的負面影響來提升用戶體驗的最簡單方式就是儘量少的去使用他們同時儘量少的請求樣式信息。這樣瀏覽器就能夠優化重排。由於重繪與重排沒法避免,只能儘量少地去使用。文章也給出具體的作法。ajax
利用工具:Firefox中的MozAfterPaint,但它僅僅可以展示重繪的內容。DynaTrace Ajax和Google發佈的SpeedTracer是兩個很是棒的深刻了解重繪和重排的工具-前者是IE使用,後者是WebKit。chrome
原文:Rendering: repaint, reflow/relayout, restyle瀏覽器
譯文:重繪重排重渲染
不一樣的瀏覽器工做方式是不同的,下面的圖表提供了渲染時的共同實現,一旦他們已經下載好了你頁面的代碼,多半都會經過瀏覽器這樣實現。
瀏覽器把HTML源代碼解析,而且建立一個DOM樹(DOM tree)-每一個HTML標籤在這個樹上都有一個對應的節點,標籤中的文本也有一個相應的文本節點。DOM樹上的根節點是documentElement
(也就是<html></html>
)
瀏覽器解析CSS代碼,經過一堆hack使得它變得有意義,諸如-moz
,-webkit
和其它不能理解的拓展名,瀏覽器會大膽的把他們忽略。CSS級聯的優先級從低到高是:瀏覽器默認的基本規則,用戶引用的樣式表,優先級最高的的是外部引入的而且最終被加入到HTML標籤的style屬性中的行內樣式。
接下來就是有趣的部分-建立一個渲染樹(render tree)。渲染樹相似DOM樹,可是並非一一對應的。渲染樹基於樣式,因此若是你使用display:none
屬性來隱藏一個div
,那麼它不會被呈如今渲染樹中。其它像head
標籤和全部在其中不可見的元素也同樣。另外一方面,DOM元素可能在渲染樹中存在多個節點,好比說每行在<p></p>
中的文本都須要一個渲染節點。渲染樹上的節點被稱爲一個frame
或者一個box
(跟CSS box相似,相關內容可查閱the box model)。每個節點都有CSS box的屬性-寬度、高度、邊框、外邊距等等。
一旦渲染樹被建立成功,瀏覽器就能夠在屏幕上繪製渲染樹節點。
讓咱們舉個例子。
HTML源碼:
<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></div> ... </body> </html>
呈現這個HTML文檔的DOM樹對每個標籤和標籤間每一段文本都有一個對應的節點。(爲了簡便起見,咱們暫且忽略空白符也是文本節點。)
documentElement (html)
head
title
body
p
[text node]
div
[text node]
div
img
...
渲染樹就是DOM樹中可見的部分。它缺乏一些元素-好比head
和隱藏的div
,可是它對文本有額外的節點(又名frame/box)。
root (RenderView) body p line 1 line 2 line 3 ... div img ...
渲染樹的根節點是包含的全部其它元素的frame/box。你能夠把它理解爲限制在瀏覽器頁面範圍內的窗口區域。技術上來講,WebKit把根節點稱爲RenderView
,對應於CSS的初始化包含塊(initial containing block),也就是從視窗範圍內頁面(0, 0)
到(window.innerWidth
, window.innerHeight
)的矩形顯示區域。
搞清楚怎樣呈現和呈現什麼須要遞歸的遍歷渲染樹。
不管什麼時候總會有一個初始化的頁面佈局伴隨着一次繪製。(除非你但願你的頁面是空白的:))以後,每一次改變用於構建渲染樹的信息都會致使如下至少一個的行爲:
部分渲染樹(或者整個渲染樹)須要從新分析而且節點尺寸須要從新計算。這被稱爲重排。注意這裏至少會有一次重排-初始化頁面佈局。
因爲節點的幾何屬性發生改變或者因爲樣式發生改變,例如改變元素背景色時,屏幕上的部份內容須要更新。這樣的更新被稱爲重繪。
重排和重繪代價是高昂的,它們會破壞用戶體驗,而且讓UI展現很是遲緩。
任何改變用來構建渲染樹的信息都會致使一次重排或重繪。
添加、刪除、更新DOM節點
經過display: none
隱藏一個DOM節點-觸發重排和重繪
經過visibility: hidden
隱藏一個DOM節點-只觸發重繪,由於沒有幾何變化
移動或者給頁面中的DOM節點添加動畫
添加一個樣式表,調整樣式屬性
用戶行爲,例如調整窗口大小,改變字號,或者滾動。
讓咱們來看一些例子:
var bstyle = document.body.style; // cache bstyle.padding = "20px"; // 重排+重繪 bstyle.border = "10px solid red"; // 另外一次重排+重繪 bstyle.color = "blue"; // 沒有尺寸變化,只重繪 bstyle.backgroundColor = "#fad"; // 重繪 bstyle.fontSize = "2em"; // 重排+重繪 // 新的DOM節點 - 重排+重繪 document.body.appendChild(document.createTextNode('dude!'));
一些重排可能開銷更大。想象一下渲染樹,若是你直接改變body下的一個子節點,可能並不會對其它節點形成影響。可是當你給一個當前頁面頂級的div添加動畫或者改變它的大小,就會推進整個頁面改變-聽起來代價就十分高昂。
既然渲染樹變化伴隨的重排和重繪代價如此巨大,瀏覽器一直致力於減小這些消極的影響。一個策略就是乾脆不作,或者說至少如今不作。瀏覽器會基於你的腳本要求建立一個變化的隊列,而後分批去展示。經過這種方式許多須要一次重排的變化就會整合起來,最終只有一次重排會被計算渲染。瀏覽器能夠向已有的隊列中添加變動並在一個特定的時間或達到一個特定數量的變動後執行。
可是有時你的代碼會組織瀏覽器優化重排並當即刷新隊列,與此同時展現全部批次的變化。這一般發生在你請求樣式信息的時候,例如:
1.offsetTop, offsetLeft, offsetWidth, offsetHeight 2.scrollTop/Left/Width/Height 3.clientTop/Left/Width/Height 4.getComputedStyle(), or currentStyle in IE
以上就是一個節點基本的可請求信息。不管合適你發出請求,瀏覽器不得不提供給你最新的值。爲了實現這一目的,瀏覽器須要應用全部隊列中的變動,刷新隊列而後去實現重排。
舉個例子,同時去set和get樣式是很糟糕的想法:
// no-no! el.style.left = el.offsetLeft + 10 + "px";
經過減小重排/重繪的負面影響來提升用戶體驗的最簡單方式就是儘量少的去使用他們同時儘量少的請求樣式信息。這樣瀏覽器就能夠優化重排。如何實踐呢?
cssText
變量中編輯。// bad var left = 10, top = 10; el.style.left = left + "px"; el.style.top = top + "px"; // better el.className += " theclassname"; // 當top和left的值是動態計算而成時... // better el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
documentFragment
來保留臨時變更。經過display:none
屬性隱藏元素(只有一次重排重繪),添加足夠多的變動後,經過display
屬性顯示(另外一次重排重繪)。經過這種方式即便大量變動也只觸發兩次重排。
不要頻繁計算樣式。若是你有一個樣式須要計算,只取一次,將它緩存在一個變量中而且在這個變量上工做。看一下下面這個反例
// no-no! for(big; loop; here) { el.style.left = el.offsetLeft + 10 + "px"; el.style.top = el.offsetTop + 10 + "px"; } // better 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"; }
就在一年前,尚未什麼瀏覽器工具可以展現繪製和渲染的過程。可是如今已經有了不少很是酷的工具。
第一個是Firefox中的MozAfterPaint,但它僅僅可以展示重繪的內容。
DynaTrace Ajax和最近Google發佈的SpeedTracer是兩個很是棒的深刻了解重繪和重排的工具-前者是IE使用,後者是WebKit。
去年的某個時候Douglas Crockford發現咱們在CSS中作了一些連咱們本身也不知道的蠢事情。我也深有體會。我以前陷入了一個難題,當在IE6中改變字體大小的時候會致使CPU佔用飆升至100%,而且在大概10-15分鐘後才能重繪整個頁面。
如今有了這些工具,咱們再也沒有藉口在CSS中作這些愚蠢的事情。
除此之外,若是像Firebug這類工具除DOM樹之外還展現了渲染樹不是很酷嗎?
讓咱們來看看這些工具該而且演示一下restyle(不影響幾何形狀的渲染樹變化)、reflow(重排,影響佈局)和repaint(重繪)。
咱們來比較一下兩種方式。第一種,我在不改變佈局的狀況下改變一些樣式,同時在每一個變動以後,咱們檢查一下樣式屬性,這些變動之間徹底沒有聯繫。
bodystyle.color = 'red'; tmp = computed.backgroundColor; bodystyle.color = 'white'; tmp = computed.backgroundImage; bodystyle.color = 'green'; tmp = computed.backgroundAttachment;
一樣的變動,可是咱們僅僅在全部變動完成後再查詢樣式屬性的信息:
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, ''); }
如今兩個例子會在document被點擊後觸發。測試頁在這裏,咱們稱之爲「restyle」 - restyle.html。
第二個測試和第一個相似,可是此次咱們還改變的佈局信息:
// touch styles every time 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; // touch at the end 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測試」,源代碼在這.
這是你在restyle測試中用DynaTrace看到的可視化結果。
頁面加載完以後,我點擊一次去執行第一個方案(修改完後當即請求樣式信息,在大概兩秒處),而後再次點擊去執行第二方案(樣式變動完成後再請求樣式信息,在大概四秒處)。
這個工具爲咱們展現了頁面是怎樣加載的和IE的logo正處於加載狀態。而後鼠標光標放置於渲染事件部分。放大後能看到更詳細的信息:
你能夠很是清楚的看到藍色條,表明JavaScript事件,還有下面的綠色條,表明渲染事件。如今,這是個很簡單的例子,可是仍然須要注意那些條的長度 - 渲染比執行JavaScript消耗多了多長時間。在Ajax/富應用中,JavaScript不是瓶頸,DOM的訪問、操做和渲染纔是。
好了,如今咱們來運行一下"relayout測試",它改變了body的幾何形狀。此次咱們在「PurePaths」視圖中檢查。這是一條能夠查看每項元素的信息的時間線。我已經高亮了第一次點擊,它做爲JavaScript事件提供了一個定時的佈局任務。
再一次的放大這個有趣的部分,你能夠看到除了「繪製」條之外,還有一個新的「計算流佈局」,這是由於在這個測試中,咱們除了重繪一樣觸發了重排。
如今讓咱們在Chrome中測試同一個頁面,而後看看SpeedTracer的結果。
這是第一個「restyle」測試放大到了有趣的部分,看看發生了什麼。
整體來看就是一次點擊和一次繪製。可是在第一次點擊的時候,一樣有一半的時間消耗在從新計算樣式。爲何會這樣呢?這是由於咱們在每次變動時就請求一次樣式信息。
展開事件會出現隱藏的線(灰色的線因爲很快會被Speedtracer隱藏),咱們可以看到發生了什麼 - 在第一次點擊後,樣式被計算了三次。而第二種方案只計算了一次。
如今讓咱們運行「relayout測試」。整個列表看起來是同樣的:
可是詳細的視圖展現了第一次點擊後致使了三次重排而第二次點擊只致使了一次重排。這很清楚的展現了發生了什麼。
這兩個工具備一些微小的不一樣 - SpeedTracer不會展現佈局任務什麼時候被執行和放入隊列,而DynaTrace會。DynaTrace不會區分restyle
和reflow/repaint
的區別,而SpeedTracer會。可能IE對這二者並無作區分吧。DynaTrace在兩次測試中並不會由於重排次數不一樣而顯示三次重排或者一次,可能IE工做原理本就如此?
即便運行上述測試幾百次,IE瀏覽器仍然不關心你在改變樣式後是否請求樣式信息。
屢次運行這些測試後,如下是得出的結論:
在chrome相較於改變樣式後當即請求樣式信息,等待樣式修改完成後檢查,在restyle
測試中會快2.5倍,在relayout
測試中會快4.42倍。
在Fifefox中,restyle測試快1.87倍,relayout測試快4.64倍。
IE6和IE8中,沒有什麼區別。
在全部瀏覽器中改變樣式僅僅須要改變樣式和佈局的一半時間。(雖然我這麼說,但實際上我僅僅比較了改變樣式和改變佈局。)除了在IE6中改變佈局會以改變樣式四倍的代價來消耗資源。
很是感謝你看完了這篇很長的文章。但願你們注意到重排這個特性。做爲總結,我來從新解釋一下術語。
渲染樹 - DOM樹的可見部分。
渲染樹上的節點被稱爲box
或ifame
從新計算渲染樹被稱爲reflow
(在Firefox中),在其它瀏覽器中被成爲relayout
更新屏幕中的顯示結果而且從新計算渲染樹被成爲repaint
或者在IE/DynaTrace中被稱爲redraw
SpeedTracer將沒有幾何變化的樣式變更稱爲樣式重計算
,將有幾何變更的稱爲layout
若是你想更深層次的去了解這些,能夠參考如下資料:
Mozilla: notes on reflow
David Baron of Mozilla: Google tech talk on Layout Engine Internals for Web Developers
WebKit: rendering basics - 6-part series of posts
Opera: repaints and reflows is a part of an article on efficient JavaScript
Dynatrace: IE rendering behavior