【高性能JS】重繪歧蕉、重排與瀏覽器優(yōu)化方法

基礎知識

瀏覽器下載完頁面中的所有組件--HTML標記灾部、JS、CSS惯退、圖片--之后會解析并生成兩個內部數(shù)據(jù)結構:

  • DOM 樹:表示頁面結構
  • 渲染樹:表示DOM節(jié)點如何顯示

網(wǎng)頁生成的過程

  1. HTML被HTML解析器解析成DOM 樹
  2. css被css解析器解析成CSSOM(CSS Object Model)
  3. attachment DOM 樹和CSSOM赌髓,生成渲染樹(Render Tree)
  4. 生成布局(flow),即將所有渲染樹的所有節(jié)點進行平面合成
  5. 將布局繪制(paint)在屏幕上

"生成布局"(flow)和"繪制"(paint)這兩步催跪,合稱為"渲染"(render)锁蠕。

網(wǎng)頁生成的時候,至少會渲染一次懊蒸。用戶訪問的過程中荣倾,還會不斷重新渲染。

image

節(jié)點定義

DOM 樹種的每一個需要顯示的節(jié)點在渲染樹中至少存在一個對應的節(jié)點(隱藏的DOM元素在渲染樹中沒有對應的節(jié)點)骑丸。

渲染樹中的節(jié)點稱為“幀(frames)”或“盒(boxes)”舌仍,符合CSS模型的定義鳖孤。

重排和重繪

定義

  • 重排是什么:重新生成布局。當DOM 的變化影響了元素的幾何屬性(寬和高)--比如改變邊框寬度或給段落增加文字導致行數(shù)增加--瀏覽器需要重新計算元素的幾何屬性抡笼,同樣其他元素的幾何屬性和位置也會因此受到影響苏揣。瀏覽器會使渲染樹中受到影響的部分失效,并重新構造渲染樹推姻。這個過程稱為重排平匈。

  • 重繪是什么:重新繪制。完成重排后藏古,瀏覽器會重新繪制受影響的部分到屏幕中增炭。這個過程稱為重繪。

重排與重繪的關系

重排一定會導致重繪拧晕,重繪不一定導致重排隙姿。如果DOM變化不影響幾何屬性,元素的布局沒有改變厂捞,則只發(fā)生一次重繪(不需要重排)输玷。

發(fā)生重排的情況

當頁面布局和幾何屬性改變時發(fā)生“重排”。如下:

  • 添加或刪除可見的DOM 元素
  • 元素位置改變
  • 元素尺寸改變(包括外邊距靡馁、內邊距欲鹏、邊框厚度、寬度臭墨、高度等屬性改變)
  • 內容改變赔嚎,例如:文本改變后圖片被另一個不同尺寸的圖片替代
  • 頁面渲染器初始化
  • 瀏覽器窗口尺寸改變

發(fā)生重排的范圍

整個頁面或局部。例如:當滾動條出現(xiàn)時觸發(fā)整個頁面的重排胧弛。

對性能的影響

重排和重繪會不斷觸發(fā)尤误,這是不可避免的。但是结缚,它們非常耗費資源损晤,是導致網(wǎng)頁性能低下的根本原因。

提高網(wǎng)頁性能掺冠,就是要降低"重排"和"重繪"的頻率和成本沉馆,盡量少觸發(fā)重新渲染码党。

渲染樹變化的排隊

前面提到德崭,DOM變動和樣式變動,都會觸發(fā)重新渲染揖盘。但是眉厨,瀏覽器已經(jīng)很智能了,會盡量把所有的變動集中在一起兽狭,排成一個隊列憾股,然后一次性執(zhí)行鹿蜀,盡量避免多次重新渲染。

div.style.color = 'blue';
div.style.marginTop = '30px';

上面代碼中服球,div元素有兩個樣式變動茴恰,但是瀏覽器只會觸發(fā)一次重排和重繪。

如果寫得不好斩熊,就會觸發(fā)兩次重排和重繪往枣。

div.style.color = 'blue';
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + 'px';

上面代碼對div元素設置背景色以后,第二行要求瀏覽器給出該元素的位置粉渠,所以瀏覽器不得不立即重排分冈。

強制刷新隊列
獲取布局信息的操作會導致列隊刷新,以下屬性和方法需要返回最新的布局信息霸株,最好避免使用雕沉。

offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle() (currentStyle in IE)

clientTop:元素上邊框的厚度,當沒有指定邊框厚底時去件,一般為0坡椒。

scrollTop:位于對象最頂端和窗口中可見內容的最頂端之間的距離,簡單地說就是滾動后被隱藏的高度尤溜。

offsetTop:獲取對象相對于由offsetParent屬性指定的父坐標(css定位的元素或body元素)距離頂端的高度肠牲。

clientHeight:內容可視區(qū)域的高度,也就是說頁面瀏覽器中可以看到內容的這個區(qū)域的高度靴跛,一般是最后一個工具條以下到狀態(tài)欄以上的這個區(qū)域缀雳,與頁面內容無關。

scrollHeight:IE梢睛、Opera 認為 scrollHeight 是網(wǎng)頁內容實際高度肥印,可以小于 clientHeight。FF 認為 scrollHeight 是網(wǎng)頁內容高度绝葡,不過最小值是 clientHeight深碱。

offsetHeight:獲取對象相對于由offsetParent屬性指定的父坐標(css定位的元素或body元素)的高度。IE藏畅、Opera 認為 offsetHeight = clientHeight + 滾動條 + 邊框敷硅。FF 認為 offsetHeight 是網(wǎng)頁內容實際高度,可以小于clientHeight愉阎。offsetHeight在新版本的FF和IE中是一樣的绞蹦,表示網(wǎng)頁的高度,與滾動條無關榜旦,chrome中不包括滾動條幽七。

Window.getComputedStyle()方法返回一個對象,該對象在應用活動樣式表并解析這些值可能包含的任何基本計算后報告元素的所有CSS屬性的值溅呢。 私有的CSS屬性值可以通過對象提供的API或通過簡單地使用CSS屬性名稱進行索引來訪問澡屡。

解決辦法

所以猿挚,從性能角度考慮,盡量不要把讀操作和寫操作驶鹉,放在一個語句里面绩蜻。

// bad
div.style.left = div.offsetLeft + 10 + "px";
div.style.top = div.offsetTop + 10 + "px";

// good
var left = div.offsetLeft;
var top  = div.offsetTop;
div.style.left = left + 10 + "px";
div.style.top = top + 10 + "px";

一般的規(guī)則是:

  • 樣式表越簡單,重排和重繪就越快室埋。
  • 重排和重繪的DOM元素層級越高辜羊,成本就越高。
  • table元素的重排和重繪成本词顾,要高于div元素八秃。

瀏覽器優(yōu)化方法

1. 減少布局信息的獲取次數(shù),獲取后賦值給局部變量肉盹,操作局部變量
當查詢布局信息時昔驱,比如獲取偏移量(offset)、滾動位置(scroll)或計算出的樣式值(computedstyle values)時上忍,瀏覽器為了返回最新值骤肛,會刷新隊列并應用所有變更。不利于優(yōu)化窍蓝。
所以應該盡量減少布局信息的獲取次數(shù)腋颠,獲取后把它賦值給局部變量,然后再操作局部變量

// 優(yōu)化前
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {
    stopAnimation();
}

// 優(yōu)化后
// 獲取一次起始位置的值吓笙,然后賦值給一個變量淑玫,在動畫循環(huán)中直接使用變量不再查詢偏移量
var current = myElement.offsetLeft;
current++;
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (myElement.offsetLeft >= 500) {
    stopAnimation();
}

2. 合并多次對DOM 和樣式的修改:使用cssText屬性

現(xiàn)在大部分瀏覽器都自動優(yōu)化了

// 優(yōu)化前
 var el = document.getElementById('mydiv');
 el.style.borderLeft = '1px';
 el.style.borderRight = '2px';
 el.style.padding = '5px';
 
 // 優(yōu)化后
 var el = document.getElementById('mydiv');
 el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

3. 合并樣式的修改時:修改css的class名稱而不是修改內聯(lián)樣式

 var el = document.getElementById('mydiv');
 el.className = "active";

4. 使元素脫離文檔流、對其改變后再把元素帶回文檔中

var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data); // 更新指定節(jié)點數(shù)據(jù)的函數(shù)
ul.style.display = 'block';

5. (推薦使用)在文檔之外創(chuàng)建并更新一個文檔片段面睛,然后把它附加到原始列表中

文檔片段是個輕量級的document對象絮蒿,用于更新和移動節(jié)點。當你附加一個片段到節(jié)點中叁鉴,實際上添加的是該片段的子節(jié)點土涝,而不是片段本身。

該方法產(chǎn)生的DOM遍歷和重排次數(shù)最少幌墓。

//創(chuàng)建一個文檔片段
var fragment = document.createDocumentFragment();

// 更新文檔片段的數(shù)據(jù)
appendDataToElement(fragment, data);

// 將文檔片段附加到原始列表中(實際添加的是子節(jié)點)
document.getElementById('mylist').appendChild(fragment);

實例如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>使用fragment進行重排重繪</title>
</head>
<body>
<ul id="myList">
  <li>a</li>
  <li>b</li>
</ul>
<p>
  向上面的ul中加入兩個新的li但壮,比較使用fragment和不使用的性能
</p>

<script>
  console.time(0);
  var newLi1 = document.createElement('li');
  newLi1.innerHTML = 'c';

  var newLi2 = document.createElement('li');
  newLi2.innerHTML = 'd';

  document.getElementById('myList').appendChild(newLi1);
  document.getElementById('myList').appendChild(newLi2);
  console.timeEnd(0)

  console.time(1);
  var fragment = document.createDocumentFragment();
  var newLi1 = document.createElement('li');
  newLi1.innerHTML = 'c';

  var newLi2 = document.createElement('li');
  newLi2.innerHTML = 'd';

  fragment.appendChild(newLi1);
  fragment.appendChild(newLi2);

  document.getElementById('myList').appendChild(fragment);
  console.timeEnd(1)
</script>
</body>
</html>


6. 備份一個節(jié)點,對副本操作常侣,完成后用副本節(jié)點代替舊節(jié)點

var old = document.getElementById('mylist');

// 對舊節(jié)點備份
var clone = old.cloneNode(true);

appendDataToElement(clone, data);

// 用副本節(jié)點代替舊節(jié)點
old.parentNode.replaceChild(clone, old);

7. 讓元素脫離動畫流
許多展開區(qū)域的幾何動畫會將頁面其他部分推向下方蜡饵。一般來說,重排只影響渲染樹中的一部分袭祟,但是也可能影響很大的部分验残。
當頁面頂部的一個動畫推移頁面整個余下的部分時,會導致一次代價昂貴的大規(guī)模重排巾乳。
使用以下步驟可以避免頁面中的大部分重排:

  1. 使用絕對位置定位頁面上的動畫元素您没,將其脫離文檔流
  2. 讓元素動起來。當它擴大時胆绊,會臨時覆蓋部分頁面氨鹏。但這只是頁面一個小區(qū)域的重繪過程,不會產(chǎn)生重排并重繪頁面的大部分內容压状。
  3. 當動畫結束時恢復定位仆抵,從而只會下移一次文檔的其他元素。
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末种冬,一起剝皮案震驚了整個濱河市镣丑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌娱两,老刑警劉巖莺匠,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異十兢,居然都是意外死亡趣竣,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門旱物,熙熙樓的掌柜王于貴愁眉苦臉地迎上來遥缕,“玉大人,你說我怎么就攤上這事宵呛〉ハ唬” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵宝穗,是天一觀的道長封孙。 經(jīng)常有香客問我,道長讽营,這世上最難降的妖魔是什么虎忌? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮橱鹏,結果婚禮上膜蠢,老公的妹妹穿的比我還像新娘。我一直安慰自己莉兰,他們只是感情好挑围,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著糖荒,像睡著了一般杉辙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上捶朵,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天蜘矢,我揣著相機與錄音狂男,去河邊找鬼。 笑死品腹,一個胖子當著我的面吹牛岖食,可吹牛的內容都是我干的。 我是一名探鬼主播舞吭,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼泡垃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了羡鸥?” 一聲冷哼從身側響起蔑穴,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惧浴,沒想到半個月后存和,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡赶舆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年哑姚,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芜茵。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡叙量,死狀恐怖,靈堂內的尸體忽然破棺而出九串,到底是詐尸還是另有隱情绞佩,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布猪钮,位于F島的核電站品山,受9級特大地震影響,放射性物質發(fā)生泄漏烤低。R本人自食惡果不足惜肘交,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扑馁。 院中可真熱鬧涯呻,春花似錦、人聲如沸腻要。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雄家。三九已至效诅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乱投。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工咽笼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人篡腌。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓褐荷,卻偏偏與公主長得像勾效,于是被迫代替她去往敵國和親嘹悼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容