改進渲染效能範例 1 - News Aggregator

使用 News Aggregator 作為改進渲染效能的範例,以下列出需要改進之處與解法,並附上測試結果。

這個範例主要展示

衡量準則

#1 改進一:向下滾動整個頁面時,怎麼卡卡的?

向下滾動整個頁面

注意這些 story 最前面的 badge .story__score,它們會隨著所在視窗的高低而呈現不同的深淺變化,上面較淺,下面較深。

檢視 Chrome DevTools Timeline,發現好多紅色的警示。

改進渲染效能範例,向下滾動整個頁面

這是由於向下滾動頁面時觸發 scroll event,會呼叫 colorizeAndScaleStories,其中會不斷的讀取目前 getBoundingClientRect 的值並做計算和寫入透明度,導致 Forced Synchronous Layout(FSL),不停的 FSL,最後造成 Layout Thrashing。

改進渲染效能範例,向下滾動整個頁面

更新頻率約 11 ~ 14fps。

拔掉這個功能,馬上順很多。

拔掉 colorizeAndScaleStories 這個 function,刪除 main.addEventListener('scroll', function() {...} 中呼叫 colorizeAndScaleStories() 的部份,並刪除 onStoryData 的最後一段。

// 刪除這一段

if (storyLoadCount === 0) {
  colorizeAndScaleStories();
}

這個功能的完整修改請見這裡

改進渲染效能範例,向下滾動整個頁面

更新頻率提高為 45fps。

#2 改進二:點擊項目,打開內容頁,怎麼又是卡卡的?

打開內容頁的過程,明顯有卡卡的狀況。

點擊項目,打開內容頁

檢視 Chrome DevTools Timeline。

改進渲染效能範例,點擊項目,打開內容頁

右上角紅色三角形警示記號表示這裡有 FSL 的狀況。這一段程式碼是在設定 storyDetails 的位置,先從 getBoundingClientRect() 取得 position left 的值,然後根據比例(也就是當時 left 值減去其十分之一)設定下一次 storyDetails 移動的目標位置,導致不停強迫瀏覽器在此幀(Frame)必須取得最新值後又馬上更新樣式,直到 storyDetails 的 left 為零為止。

var storyDetailsPosition = storyDetails.getBoundingClientRect(); // 讀取

// 略

storyDetails.style.left = left + 'px'; // 寫入

點此看原始碼。

改進渲染效能範例,點擊項目,打開內容頁

animate 這個 function 總共花了 47.7ms(通常會小於 3ms),畫面更新頻率是 13fps。

因此,針對這個功能的改善目標為

修改如下。

方法一

捨棄 setTimeout,改用 requestAnimationFrame,讓瀏覽器自行決定執行 animate 的時機,恰巧消除了 FSL,更新頻率為 36fps。

捨棄 setTimeout,改用 requestAnimationFrame,讓瀏覽器自行決定執行時機。

捨棄 setTimeout,改用 requestAnimationFrame,讓瀏覽器自行決定執行時機。

但仍未達到 60fps,來看方法二。

方法二

.story-details {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  justify-content: flex-start;
  align-items: stretch;
  align-content: stretch;
  position: fixed;
  top: 0;
  width: 100%;
  height: 100%;
  background: white;
  z-index: 2;
  box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.06), 0px 2px 5px 0px rgba(0, 0, 0, 0.08),
    0px 2px 7px 0px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  transition: transform 0.3s;
  will-change: transform;
}

.visible {
  transform: translateX(0);
}

.hidden {
  transform: translateX(100%);
}

移除對每一個 story-details 的子元素升階,這會造成過多的圖層,造成合成效能不佳。

.story-details * {
  will-change: transform;
  transform: translateZ(0);
}

由於改用 transform 來打開 storyDetail,因此也要改寫 onStoryClick、砍掉 animate;在建立 storyDetail 時先放在畫面之外,待點擊後再移到畫面中。其實這裡不需要為每個項目都建立自己的 storyDetail,而可以只建立一個,之後替換內容即可,這樣就可以避免 DOM Element 的操作和產生更多圖層、增加合成的負擔。

function onStoryClick(details) {
  if (inDetails) {
    return;
  }
  inDetails = true;

  var storyDetails = $('#sd-' + details.id);

  setTimeout(showStory.bind(this, storyDetails), 60);

  if (!storyDetails) {
    // 省略...

    storyDetails = document.createElement('section');
    storyDetails.setAttribute('id', 'sd-' + details.id);
    storyDetails.classList.add('story-details');
    storyDetails.classList.add('hidden');
    storyDetails.innerHTML = storyDetailsHtml;
    document.body.appendChild(storyDetails);

    // 省略...

    storyDetails.classList.add('visible');
    storyDetails.classList.remove('hidden');

    // 省略...
  }
}

function showStory(storyDetails) {
  if (!storyDetails) {
    return;
  }
  storyDetails.classList.remove('hidden');
  storyDetails.classList.add('visible');
}

function hideStory(storyDetails) {
  storyDetails.classList.add('hidden');
  storyDetails.classList.remove('visible');
  inDetails = false;
}

這個功能的完整修改請見這裡

改進渲染效能範例,點擊項目,打開內容頁

每個 Frame 經歷各階段所花的時間如下圖,重新計算樣式、更新 Render Tree 和合成,沒有版面配置(Layout)和繪製(Paint),更新頻率超過 60fps。

改進渲染效能範例,點擊項目,打開內容頁

待更新

仍有一些地方是可以改善的…見 full list of bugs

官方解答


效能調校 轉譯效能 關鍵轉譯路徑 Rendering Performance Critical Rendering Path Forced Synchronous Layout Layout Thrashing Chrome DevTools will-change requestAnimationFrame css css3 animations 前端效能 系列文