渲染篇五:CSS的回流(Reflow)與重繪(Repaint)

說(shuō)明:該文章是我在掘金購(gòu)買(mǎi)的一本小冊(cè)《前端性能優(yōu)化原理與實(shí)踐》中關(guān)于回流與重繪的介紹,貼出來(lái)次舌,供更多人閱讀理解烹困、學(xué)習(xí)诀艰。

回流:當(dāng)我們對(duì) DOM 的修改引發(fā)了 DOM 幾何尺寸的變化(比如修改元素的寬偎窘、高或隱藏元素等)時(shí),瀏覽器需要重新計(jì)算元素的幾何屬性(其他元素的幾何屬性和位置也會(huì)因此受到影響),然后再將計(jì)算的結(jié)果繪制出來(lái)缠犀。這個(gè)過(guò)程就是回流(也叫重排)。

重繪:當(dāng)我們對(duì) DOM 的修改導(dǎo)致了樣式的變化聪舒、卻并未影響其幾何屬性(比如修改了顏色或背景色)時(shí)辨液,瀏覽器不需重新計(jì)算元素的幾何屬性、直接為該元素繪制新的樣式(跳過(guò)了上圖所示的回流環(huán)節(jié))箱残。這個(gè)過(guò)程叫做重繪滔迈。

由此我們可以看出,重繪不一定導(dǎo)致回流被辑,回流一定會(huì)導(dǎo)致重繪燎悍。硬要比較的話,回流比重繪做的事情更多盼理,帶來(lái)的開(kāi)銷(xiāo)也更大谈山。但這兩個(gè)都會(huì)耗費(fèi)性能的,所以我們?cè)陂_(kāi)發(fā)中宏怔,要從代碼層面出發(fā)奏路,盡可能把回流和重繪的次數(shù)最小化。

哪些實(shí)際操作會(huì)導(dǎo)致回流與重繪

觸發(fā)重繪的“導(dǎo)火索”比較好識(shí)別——只要是不觸發(fā)回流臊诊,但又觸發(fā)了樣式改變的 DOM 操作思劳,都會(huì)引起重繪,比如背景色妨猩、文字色潜叛、可見(jiàn)性(可見(jiàn)性這里特指形如visibility: hidden這樣不改變?cè)匚恢煤痛嬖谛缘摹渭冡槍?duì)可見(jiàn)性的操作壶硅,注意與display:none進(jìn)行區(qū)分)等威兜。為此,我們要著重理解一下那些可能觸發(fā)回流的操作庐椒。

回流的“導(dǎo)火索”
最“貴”的操作:改變 DOM 元素的幾何屬性

這個(gè)改變幾乎可以說(shuō)是“牽一發(fā)動(dòng)全身”——當(dāng)一個(gè)DOM元素的幾何屬性發(fā)生變化時(shí)椒舵,所有和它相關(guān)的節(jié)點(diǎn)(比如父子節(jié)點(diǎn)、兄弟節(jié)點(diǎn)等)的幾何屬性都需要進(jìn)行重新計(jì)算约谈,它會(huì)帶來(lái)巨大的計(jì)算量笔宿。
常見(jiàn)的幾何屬性有 width犁钟、height、padding泼橘、margin涝动、left、top炬灭、border 等等醋粟。此處不再給大家一一列舉。有的文章喜歡羅列屬性表格重归,但我相信我今天列出來(lái)大家也不會(huì)看米愿、看了也記不住(因?yàn)樘嗔耍┍撬薄N易约阂膊粫?huì)去記這些——其實(shí)確實(shí)沒(méi)必要記育苟,?一個(gè)屬性是不是幾何屬性、會(huì)不會(huì)導(dǎo)致空間布局發(fā)生變化椎木,大家寫(xiě)樣式的時(shí)候完全可以通過(guò)代碼效果看出來(lái)违柏。多說(shuō)無(wú)益,還希望大家可以多寫(xiě)多試拓哺,形成自己的“肌肉記憶”勇垛。

“價(jià)格適中”的操作:改變 DOM 樹(shù)的結(jié)構(gòu)

這里主要指的是節(jié)點(diǎn)的增減脖母、移動(dòng)等操作士鸥。瀏覽器引擎布局的過(guò)程,順序上可以類(lèi)比于樹(shù)的前序遍歷——它是一個(gè)從上到下谆级、從左到右的過(guò)程烤礁。通常在這個(gè)過(guò)程中,當(dāng)前元素不會(huì)再影響其前面已經(jīng)遍歷過(guò)的元素肥照。

最容易被忽略的操作:獲取一些特定屬性的值

當(dāng)你要用到像這樣的屬性:offsetTop脚仔、offsetLeft、 offsetWidth舆绎、offsetHeight鲤脏、scrollTop、scrollLeft吕朵、scrollWidth猎醇、scrollHeight、clientTop努溃、clientLeft硫嘶、clientWidth、clientHeight 時(shí)梧税,你就要注意了沦疾!

“像這樣”的屬性称近,到底是像什么樣?——這些值有一個(gè)共性哮塞,就是需要通過(guò)即時(shí)計(jì)算得到刨秆。因此瀏覽器為了獲取這些值,也會(huì)進(jìn)行回流彻桃。

除此之外坛善,當(dāng)我們調(diào)用了 getComputedStyle 方法,或者 IE 里的 currentStyle 時(shí)邻眷,也會(huì)觸發(fā)回流眠屎。原理是一樣的,都為求一個(gè)“即時(shí)性”和“準(zhǔn)確性”肆饶。

如何規(guī)避回流與重繪

了解了回流與重繪的“導(dǎo)火索”改衩,我們就要盡量規(guī)避它們。但很多時(shí)候驯镊,我們不得不使用它們葫督。當(dāng)避無(wú)可避時(shí),我們就要學(xué)會(huì)更聰明地使用它們板惑。

將“導(dǎo)火索”緩存起來(lái)橄镜,避免頻繁改動(dòng)

有時(shí)我們想要通過(guò)多次計(jì)算得到一個(gè)元素的布局位置,我們可能會(huì)這樣做:

<!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')
  // 這里循環(huán)判定比較簡(jiǎn)單冯乘,實(shí)際中或許會(huì)拓展出比較復(fù)雜的判定需求
  for(let i=0;i<10;i++) {
  el.style.top  = el.offsetTop  + 10 + "px";
  el.style.left = el.offsetLeft + 10 + "px";
  }
  </script>
</body>
</html>

這樣做洽胶,每次循環(huán)都需要獲取多次“敏感屬性”,是比較糟糕的裆馒。我們可以將其以 JS 變量的形式緩存起來(lái)姊氓,待計(jì)算完畢再提交給瀏覽器發(fā)出重計(jì)算請(qǐng)求:

// 緩存offsetLeft與offsetTop的值
const el = document.getElementById('el') 
let offLeft = el.offsetLeft, offTop = el.offsetTop

// 在JS層面進(jìn)行計(jì)算
for(let i=0;i<10;i++) {
  offLeft += 10
  offTop  += 10
}

// 一次性將計(jì)算結(jié)果應(yīng)用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop  + "px"
避免逐條改變樣式,使用類(lèi)名去合并樣式

比如我們可以把這段單純的代碼:

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

優(yōu)化成一個(gè)有 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>

前者每次單獨(dú)操作喷好,都去觸發(fā)一次渲染樹(shù)更改翔横,從而導(dǎo)致相應(yīng)的回流與重繪過(guò)程。
合并之后梗搅,等于我們將所有的更改一次性發(fā)出禾唁,用一個(gè) style 請(qǐng)求解決掉了。

將 DOM “離線”

我們上文所說(shuō)的回流和重繪无切,都是在“該元素位于頁(yè)面上”的前提下會(huì)發(fā)生的荡短。一旦我們給元素設(shè)置 display: none,將其從頁(yè)面上“拿掉”订雾,那么我們的后續(xù)操作肢预,將無(wú)法觸發(fā)回流與重繪——這個(gè)將元素“拿掉”的操作,就叫做 DOM 離線化洼哎。
仍以我們上文的代碼片段為例:

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了許多類(lèi)似的后續(xù)操作)

離線化后就是這樣:

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'
...(省略了許多類(lèi)似的后續(xù)操作)
container.style.display = 'block'

有的同學(xué)會(huì)問(wèn)烫映,拿掉一個(gè)元素再把它放回去沼本,這不也會(huì)觸發(fā)一次昂貴的回流嗎?這話不假锭沟,但我們把它拿下來(lái)了抽兆,后續(xù)不管我操作這個(gè)元素多少次,每一步的操作成本都會(huì)非常低族淮。當(dāng)我們只需要進(jìn)行很少的 DOM 操作時(shí)辫红,DOM 離線化的優(yōu)越性確實(shí)不太明顯。一旦操作頻繁起來(lái)祝辣,這“拿掉”和“放回”的開(kāi)銷(xiāo)都將會(huì)是非常值得的贴妻。

Flush 隊(duì)列:瀏覽器并沒(méi)有那么簡(jiǎn)單

Q:下面幾行代碼。觸發(fā)了幾次回流與重繪呢蝙斜?

let container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

A:“width名惩、height、border是幾何屬性孕荠,各觸發(fā)一次回流娩鹉;color只造成外觀的變化,會(huì)觸發(fā)一次重繪稚伍⊥溆瑁”

現(xiàn)代瀏覽器是很聰明的。瀏覽器自己也清楚个曙,如果每次 DOM 操作都即時(shí)地反饋一次回流或重繪锈嫩,那么性能上來(lái)說(shuō)是扛不住的。于是它自己緩存了一個(gè) flush 隊(duì)列困檩,把我們觸發(fā)的回流與重繪任務(wù)都塞進(jìn)去祠挫,待到隊(duì)列里的任務(wù)多起來(lái)那槽、或者達(dá)到了一定的時(shí)間間隔悼沿,或者“不得已”的時(shí)候,再將這些任務(wù)一口氣出隊(duì)骚灸。

更多內(nèi)容糟趾,請(qǐng)?jiān)L問(wèn)的我的個(gè)人博客:https://liugezhou.github.io/blog.
您也可以關(guān)注我的個(gè)人公眾號(hào):【W(wǎng)akaka】

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市甚牲,隨后出現(xiàn)的幾起案子义郑,更是在濱河造成了極大的恐慌,老刑警劉巖丈钙,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件非驮,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡雏赦,警方通過(guò)查閱死者的電腦和手機(jī)劫笙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)芙扎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人填大,你說(shuō)我怎么就攤上這事戒洼。” “怎么了允华?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵圈浇,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我靴寂,道長(zhǎng)磷蜀,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任百炬,我火速辦了婚禮蠕搜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘收壕。我一直安慰自己妓灌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布蜜宪。 她就那樣靜靜地躺著虫埂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪圃验。 梳的紋絲不亂的頭發(fā)上掉伏,一...
    開(kāi)封第一講書(shū)人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音澳窑,去河邊找鬼斧散。 笑死,一個(gè)胖子當(dāng)著我的面吹牛摊聋,可吹牛的內(nèi)容都是我干的鸡捐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼麻裁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼箍镜!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起煎源,我...
    開(kāi)封第一講書(shū)人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤色迂,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后手销,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體歇僧,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年锋拖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了诈悍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片埂淮。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖写隶,靈堂內(nèi)的尸體忽然破棺而出倔撞,到底是詐尸還是另有隱情,我是刑警寧澤慕趴,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布痪蝇,位于F島的核電站,受9級(jí)特大地震影響冕房,放射性物質(zhì)發(fā)生泄漏躏啰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一耙册、第九天 我趴在偏房一處隱蔽的房頂上張望给僵。 院中可真熱鬧,春花似錦详拙、人聲如沸帝际。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蹲诀。三九已至,卻和暖如春弃揽,著一層夾襖步出監(jiān)牢的瞬間脯爪,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工矿微, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痕慢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓涌矢,卻偏偏與公主長(zhǎng)得像掖举,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蒿辙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容