避免大規模、復雜的布局

1002zt 10年前發布 | 6K 次閱讀 前端技術

布局,就是瀏覽器計算DOM元素的幾何信息的過程:元素大小和在頁面中的位置。每個元素都有一個顯式或隱式的大小信息,決定于其CSS屬性的設置、或是元素本身內容的大小、抑或是其父元素的大小。在Blink/WebKit內核的瀏覽器和IE中,這個過程稱為布局。在基于Gecko的瀏覽器(比如Firefox)中,這個過程稱為Reflow。雖然稱呼不一樣,但二者在本質上是一樣的。

TL;DR

  • 布局通常是在整個文檔范圍內發生。
  • 需要布局的DOM元素的數量直接影響到性能;應該盡可能避免觸發布局。
  • 分析頁面布局模型的性能;新的Flexbox比舊的Flexbox和基于浮動的布局模型更高效。
  • 避免強制同步布局事件的發生;對于元素的樣式屬性值,要先讀再寫。

與樣式計算類似,布局的時間消耗主要在于:

  1. 需要布局的DOM元素的數量
  2. 布局過程的復雜程度

盡可能避免觸發布局

當你修改了元素的樣式屬性之后,瀏覽器會將會檢查為了使這個修改生效是否需要重新計算布局以及更新渲染樹。對于DOM元素的“幾何屬性”的修改,比如width/height/left/top等,都需要重新計算布局。

.box {
  width: 20px;
  height: 20px;
}

/**
 * Changing width and height
 * triggers layout.
 */
.box--expanded {
  width: 200px;
  height: 350px;
}

幾乎所有的布局都是在整個文檔范圍內發生的。 如果你的頁面中含有很多元素,那么計算這些元素的位置和維度的工作將耗費很長時間。

如果確實無法避免布局的發生,那么同樣,你應該使用Chrome的DevTools來分析一下它到底耗費了多長時間,從而判斷布局過程是否是頁面性能的瓶頸。首先,打開DevTools,選擇Timeline標簽,,點擊左上角紅色record按鈕,然后在頁面上做一些互動操作。再點擊一次那個紅色按鈕結束記錄,你就會看到頁面性能的分解圖:

DevTools showing a long time in Layout

我們再仔細分析一下上面的例子,會發現布局耗費的時間超過20毫秒。前面已經說過,為了保障流暢的動畫效果,我們需要控制每一幀的時間消耗在16毫秒以內,而現在這個消耗顯然太長了。我們還可以看到其他一些細節,比如布局樹的大小(此例中為1618個節點)、需要布局的DOM節點數量。

Note

  • 想要一份詳細的能觸發布局、繪制或渲染層合并的CSS屬性清單?去CSS Triggers看看吧。

使用flexbox替代老的布局模型

web頁面有許多種布局模型,瀏覽器對它們的支持程度各不相同。最老式的布局模型能以相對、絕對和浮動的方式將元素定位到屏幕上。

下圖顯示了對頁面中1300個盒對象使用浮動布局的時間消耗分析。當然這個例子有點極端,因為它只用了一種定位方式,而在大多數實際應用中會混用多種定位方式。

Using floats as layout

如果我們對這個示例中的元素使用Flexbox的布局方式(這是web平臺上最近新添加的一種布局方式),我們將得到一張完全不同的布局時間消耗圖:

Using flexbox as layout

可以看到,對_同樣數量的元素_改用Flexbox布局之后,達到了同樣的顯示效果,但是時間消耗卻得到大幅改進(由14毫秒減少到3.5毫秒)。同時需要注意的是,在有些場景下你可能無法使用Flexbox布局方式,因為它不像浮動布局那樣被瀏覽器廣泛支持。但不管怎樣,至少你得在對頁面布局模型的性能分析的基礎之上,來選擇一種性能最優的布局方式,而不是隨意地選擇布局方式。

在任何情況下,不管是是否使用Flexbox,你都應該努力避免同時觸發所有布局,特別在頁面對性能敏感的時候(比如執行動畫效果或頁面滾動時)。

避免強制同步布局事件的發生

將一幀畫面渲染到屏幕上的處理順序如下所示:

Using flexbox as layout

首先是執行JavaScript腳本,_然后_是樣式計算,_然后_是布局。但是,我們還可以強制瀏覽器在執行JavaScript腳本之前先執行布局過程,這就是所謂的強制同步布局

首先你得記住,在JavaScript腳本運行的時候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。因此,如果你想在這一幀開始的時候,讀取一個元素(暫且稱其為“box”)的height屬性,你可以會寫出這樣的JavaScript代碼:

// Schedule our function to run at the start of the frame.
requestAnimationFrame(logBoxHeight);

function logBoxHeight() {
  // Gets the height of the box in pixels and logs it out.
  console.log(box.offsetHeight);
}

如果你在讀取height屬性之前,修改了box的樣式,那么可能就會有問題了:

function logBoxHeight() {

  box.classList.add('super-big');

  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);
}

現在,為了給你返回box的height屬性值,瀏覽器必須_首先_應用box的屬性修改(因為對其添加了super-big樣式),_接著_執行布局過程。在這之后,瀏覽器才能返回正確的height屬性值。但其實我們可以避免這個不必要且耗費昂貴的布局過程。

為了避免觸發不必要的布局過程,你應該首先批量讀取元素樣式屬性(瀏覽器將直接返回上一幀的樣式屬性值),然后再對樣式屬性進行寫操作。

上面的JavaScript函數的正確寫法應該是:

function logBoxHeight() {
  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}

大多數情況下,你應該都不需要先修改然后再讀取元素的樣式屬性值,使用上一幀的值就足夠了。過早地同步執行樣式計算和布局是潛在的頁面性能的瓶頸之一,你大概也不想這樣做。

避免快速連續的布局

還有一種情況比強制同步布局更糟:連續快速的多次執行它。我們看看這段代碼:

function resizeAllParagraphsToMatchBlockWidth() {

  // Puts the browser into a read-write-read-write cycle.
  for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

這段代碼對一組段落標簽執行循環操作,設置<p>標簽的width屬性值,使其與box元素的寬度相同。看上去這段代碼是OK的,但問題在于,在每次循環中,都讀取了box元素的一個樣式屬性值,然后立即使用該值來更新<p>元素的width屬性。在下一次循環中讀取box元素offsetwidth屬性的時候,瀏覽器必須先使得上一次循環中的樣式更新操作生效,也就是執行布局過程,然后才能響應本次循環中的樣式讀取操作。也就意味著,布局過程將在_每次循環_中發生。

我們使用_先讀后寫_的原則,來修復上述代碼中的問題:

// Read.
var width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = width + 'px';
  }
}

如果你想確保編寫的讀寫操作是安全的,你可以使用FastDOM。它能幫你自動完成讀寫操作的批處理,還能避免意外地觸發強制同步布局或快速連續的布局。

來自:https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing

 

 本文由用戶 1002zt 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!