改進渲染效能範例 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.10);
  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

官方解答


comments powered by Disqus