迴流:當咱們對 DOM 的修改引起了 DOM 幾何尺寸的變化(好比修改元素的寬、高或隱藏元素等)時,瀏覽器須要從新計算元素的幾何屬性(其餘元素的幾何屬性和位置也會所以受到影響),而後再將計算的結果繪製出來。這個過程就是迴流(也叫重排)。html
重繪:當咱們對 DOM 的修改致使了樣式的變化、卻並未影響其幾何屬性(好比修改了顏色或背景色)時,瀏覽器不需從新計算元素的幾何屬性、直接爲該元素繪製新的樣式(跳過了上圖所示的迴流環節)。這個過程叫作重繪。瀏覽器
由此咱們能夠看出,重繪不必定致使迴流,迴流必定會致使重繪。硬要比較的話,迴流比重繪作的事情更多,帶來的開銷也更大。但這兩個說到底都是吃性能的,因此都不是什麼善茬。咱們在開發中,要從代碼層面出發,儘量把迴流和重繪的次數最小化。緩存
要避免迴流與重繪的發生,最直接的作法是避免掉可能會引起迴流與重繪的 DOM 操做,就好像拆彈專家在解決一顆炸彈時,最重要的是掐滅它的導火索。bash
觸發重繪的「導火索」比較好識別——只要是不觸發迴流,但又觸發了樣式改變的 DOM 操做,都會引發重繪,好比背景色、文字色、可見性(可見性這裏特指形如visibility: hidden這樣不改變元素位置和存在性的、單純針對可見性的操做,注意與display:none進行區分)等。爲此,咱們要着重理解一下那些可能觸發迴流的操做。工具
這個改變幾乎能夠說是「牽一髮動全身」——當一個DOM元素的幾何屬性發生變化時,全部和它相關的節點(好比父子節點、兄弟節點等)的幾何屬性都須要進行從新計算,它會帶來巨大的計算量。佈局
常見的幾何屬性有 width、height、padding、margin、left、top、border 等等。此處再也不給你們一一列舉。有的文章喜歡羅列屬性表格,但我相信我今天列出來你們也不會看、看了也記不住(由於太多了)。我本身也不會去記這些——其實確實不必記,️一個屬性是否是幾何屬性、會不會致使空間佈局發生變化,你們寫樣式的時候徹底能夠經過代碼效果看出來。多說無益,還但願你們能夠多寫多試,造成本身的「肌肉記憶」。性能
這裏主要指的是節點的增減、移動等操做。瀏覽器引擎佈局的過程,順序上能夠類比於樹的前序遍歷——它是一個從上到下、從左到右的過程。一般在這個過程當中,當前元素不會再影響其前面已經遍歷過的元素。優化
當你要用到像這樣的屬性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 時,你就要注意了!ui
「像這樣」的屬性,究竟是像什麼樣?——這些值有一個共性,就是須要經過即時計算獲得。所以瀏覽器爲了獲取這些值,也會進行迴流。編碼
除此以外,當咱們調用了 getComputedStyle 方法,或者 IE 裏的 currentStyle 時,也會觸發迴流。原理是同樣的,都爲求一個「即時性」和「準確性」。
瞭解了迴流與重繪的「導火索」,咱們就要儘可能規避它們。但不少時候,咱們不得不使用它們。當避無可避時,咱們就要學會更聰明地使用它們。
有時咱們想要經過屢次計算獲得一個元素的佈局位置,咱們可能會這樣作:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#el {
width: 100px;
height: 100px;
background-color: yellow;
position: absolute;
}
</style>
</head>
<body>
<div id="el"></div>
<script>
// 獲取el元素
const el = document.getElementById('el')
// 這裏循環斷定比較簡單,實際中或許會拓展出比較複雜的斷定需求
for(let i=0;i<10;i++) {
el.style.top = el.offsetTop + 10 + "px";
el.style.left = el.offsetLeft + 10 + "px";
}
</script>
</body>
</html>
複製代碼
這樣作,每次循環都須要獲取屢次「敏感屬性」,是比較糟糕的。咱們能夠將其以 JS 變量的形式緩存起來,待計算完畢再提交給瀏覽器發出重計算請求:
// 緩存offsetLeft與offsetTop的值
const el = document.getElementById('el')
let offLeft = el.offsetLeft, offTop = el.offsetTop
// 在JS層面進行計算
for(let i=0;i<10;i++) {
offLeft += 10
offTop += 10
}
// 一次性將計算結果應用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop + "px"
複製代碼
好比咱們能夠把這段單純的代碼:
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
複製代碼
優化成一個有 class 加持的樣子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.basic_style {
width: 100px;
height: 200px;
border: 10px solid red;
color: red;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
const container = document.getElementById('container')
container.classList.add('basic_style')
</script>
</body>
</html>
複製代碼
前者每次單獨操做,都去觸發一次渲染樹更改,從而致使相應的迴流與重繪過程。
合併以後,等於咱們將全部的更改一次性發出,用一個 style 請求解決掉了。
咱們上文所說的迴流和重繪,都是在「該元素位於頁面上」的前提下會發生的。一旦咱們給元素設置 display: none,將其從頁面上「拿掉」,那麼咱們的後續操做,將沒法觸發迴流與重繪——這個將元素「拿掉」的操做,就叫作 DOM 離線化。
仍以咱們上文的代碼片斷爲例:
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了許多相似的後續操做)
複製代碼
離線化後就是這樣:
let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了許多相似的後續操做)
container.style.display = 'block'
複製代碼
有的同窗會問,拿掉一個元素再把它放回去,這不也會觸發一次昂貴的迴流嗎?這話不假,但咱們把它拿下來了,後續無論我操做這個元素多少次,每一步的操做成本都會很是低。當咱們只須要進行不多的 DOM 操做時,DOM 離線化的優越性確實不太明顯。一旦操做頻繁起來,這「拿掉」和「放回」的開銷都將會是很是值得的。
以咱們如今的知識基礎,理解上面的優化操做並不難。那麼如今我問你們一個問題:
let container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
複製代碼
這段代碼裏,瀏覽器進行了多少次的迴流或重繪呢?
「width、height、border是幾何屬性,各觸發一次迴流;color只形成外觀的變化,會觸發一次重繪。」——若是你馬上這麼想了,說明你是個能力不錯的同窗,認真閱讀了前面的內容。那麼咱們如今馬上跑一跑這段代碼,看看瀏覽器怎麼說:
這裏爲你們截取有「Layout」和「Paint」出鏡的片斷(這個圖是經過 Chrome 的 Performance 面板獲得的,後面會教你們用這個東西)。咱們看到瀏覽器只進行了一次迴流和一次重繪——和咱們想的不同啊,爲啥呢?
由於現代瀏覽器是很聰明的。瀏覽器本身也清楚,若是每次 DOM 操做都即時地反饋一次迴流或重繪,那麼性能上來講是扛不住的。因而它本身緩存了一個 flush 隊列,把咱們觸發的迴流與重繪任務都塞進去,待到隊列裏的任務多起來、或者達到了必定的時間間隔,或者「不得已」的時候,再將這些任務一口氣出隊。所以咱們看到,上面就算咱們進行了 4 次 DOM 更改,也只觸發了一次 Layout 和一次 Paint。
你們這裏尤爲當心這個「不得已」的時候。前面咱們在介紹迴流的「導火索」的時候,提到過有一類屬性很特別,它們有很強的「即時性」。當咱們訪問這些屬性時,瀏覽器會爲了得到此時此刻的、最準確的屬性值,而提早將 flush 隊列的任務出隊——這就是所謂的「不得已」時刻。具體是哪些屬性值,咱們已經在「最容易被忽略的操做」這個小模塊介紹過了,此處再也不贅述。
整個一節讀下來,可能會有同窗感到疑惑:既然瀏覽器已經爲咱們作了批處理優化,爲何咱們還要本身操心這麼多事情呢?今天避免這個明天避免那個,多麻煩!
問題在於,並非全部的瀏覽器都是聰明的。咱們剛剛的性能圖表,是 Chrome 的開發者工具呈現給咱們的。Chrome 裏行得通的東西,到了別處(好比 IE)就不必定行得通了。而咱們並不知道用戶會使用什麼樣的瀏覽器。若是不手動作優化,那麼一個頁面在不一樣的環境下就會呈現不一樣的性能效果,這對咱們、對用戶都是不利的。所以,養成良好的編碼習慣、從根源上解決問題,仍然是最周全的方法。