Day04 你知道 setTimout、setInterval、requestAnimationFrame API 三者的關係嗎


前言

這不是面試題,是近期實作關於動畫的開發項目時,在被同事 Code Review 時有提及的一個議題,內容是在計算動畫更新的邏輯從 setInterval 觸發改成使用 requestAnimationFrame API,因此小展開了研究之旅。 看到 Code Base 裡模組去取得各瀏覽器 requestAnimationFrame API及實作 polyfill 時我有點小傻眼了,竟然是使用 setTimeout 並指定 delay time,心裡想著是不是被耍了,不經想著如果這樣也能叫一個 API ,那我再來寫一個 callback 再回 call 自己,不就變成 requestAnimFrameLoop 了,我是不是很天才 XD ,但事情不是這麼簡單的,讓我們看下去。

window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          function( callback ){
            window.setTimeout(callback, 1000 / 60);
  };
})();

不急,我們先來看個動畫案例:Clock Canvas

直接到 Codepen 上找一個 Clock 的專案,直接給他 fork 下去,原作者是 Marco

function initCanvas(id){...省略};
function renderTime(id){...省略};

const ctx1 = initCanvas('canvas');
const renderTime1 = renderTime.bind(this, ctx1);
setInterval(renderTime, 100);

小說明程式碼
initCanas:用來配置 Canvas 樣式並取得 ctx
renderTime:用來重新計算時鐘呈現的畫面
setTinerval:每 100ms 重複執行 renderTime

討論一:更新間隔應該設定幾毫秒 (ms)

你可能會告訴我最右邊的 200ms 看起來很 lag,那我可以告訴你三個看起來都很 lag ,因為為了 gif 圖片檔快速載入,我在畫質跟 FPS 提低很多才傳的上來 XD

但說到 FPS ,全名是 Frames Per Second,在大部分裝置的固定是 60 FPS,也就是說我們要在 1000 (s) / 60 (frame) 約為 16 (ms/frame) 內完成一次畫面更新的計算,才能達到最順暢的體驗,低於過多也可能就造成多餘的資源浪費,因為多計算的畫面使用者看不到。

討論二:設定 setInterval 為 16 ms 執行一次就完美了嗎?


優化是一定有的,因為刷新畫面是有頻率自行運行,而動畫更新觸發使用 setInterval 去執行可能會發生一個情況就是 在計算新畫面時間不超過 刷新週期 16ms,但來不及被 render 的囧境,這怎麼發生的呢,假設當前 Main Thread 在處理效能極重的事件使著 setInterval 當下計算新動畫畫面要時 10 毫秒 (ms),而當下如果計算起始點在兩次 Frame 刷新之間 ,則可能趕不上在當次 Frame 刷新前完成運算。這種狀況將造成動畫偶爾是 2 Frame 一次更新 的 lag 現象,且後續那次 Frame 可能是計算了兩次動畫畫面,就又多了一計算的浪費。

開 Dev Tool 看 Timeline 會發現 Timer fire 位置在每個 Frame 週期是變動的。

使用 requestAnimationFrame API 能怎麼優化

requestAnimationFrame 就像是自動指定了 delay 的 setTimeout,但實際上不是固定的 Frame,而是會依照當前裝置的渲染率,並且自動選在每個 Frame 的開頭去執行 callback 函數,讓動畫運算函數擁有最完整的 Frame 間隔時間運算,以達到優化的效果,以下來實作看看。

先試著把 setInterval 改成 setTimeout 的程式碼。

function initCanvas(id){...省略};
function renderTime(id){...省略};

// setInterval(renderTime1, 16);

function loop(){
  renderTime();
  setTimeout(loop, 16);
}
setTimeout(loop, 16);

再把 setTimeout 換成 requestAnimationFrame,並且拿掉 delay time,就大功告成拉。

function initCanvas(id){...省略};
function renderTime(id){...省略};

function loop(){
  renderTime();
  // setTimeout(loop, 16);
  requestAnimationFrame(loop);
}
// setTimeout(loop, 16);
requestAnimationFrame(loop);

開 Dev Tool 看 Timeline 的計算位置對得整整齊齊的,而且小發現它還把多個 Task 合併成一個了,好像還有更細微的優化是我在上面沒提到的 ~

小結

回到開頭提到的 setTimoutsetIntervalrequestAnimationFrame 三者關係

  • setInterval 是重複觸發 setTimout 的方便函數
  • requestAnimationFrame 是優化動畫更新時用到的 setTimeout,並會在 Frame 週期一開始觸發。

在 Google Developer Docs 說到 針對視覺變更,請使用 RequestAnimationFrame的建議,整體上程式要調整成本也蠻少的,所以建議大家有用到不妨試試看這個 API 吧,雖然目前對我來說這 API 當作冷知識比實際帶來價值高許多,可能不是遊戲或很多蟲動畫體驗的產品網站應該不會感受到高計算效率的問題,在那個當下能完整使用 選染週期 的資源就變成一個很重要的課題,處理不好可能 FPS 從 60 掉成一半 30 也不一定。

另外而在找尋資料過程還有看到優化 計算在 Thread 卡頓的問題可以用 Worker 這個 API 來處理,或許後面幾篇文章可以來討論研究看看。謝謝大家今天看完文章,有任何想補充或討論的可以再下面留言哦。

資料來源

#Web #javascript #Web API #requestAnimationFrame #setInterval





近期正式轉職成為一個前端工程師 在工作的 Code Base 看多許多從未使用過的 Web API 想透過此次黑客松來翻翻文件 並把感覺實用、有趣的學習並紀錄起來 ~

留言討論