在 Low-level API 裡面,我們通常用 rAF (requestAnimationFrame) 在裡面計算時間戳,再加上遞迴做出 render loop,這種「每隔一段時間做某件事」的程式碼叫做 timer。理論上 timer 不只有 rAF 一個方案,而且就算不用 Low-level API,在 Rive 或是其他動畫的世界裡也常常有 timer 的需求。所以今天就想討論一下 timer 的幾個解決方案,到底哪一種比較好。
setInterval 應該是最直覺的方案,setTimeout 雖然要用遞迴去寫,但有一些基本概念或是搭配 AI 輔助,應該也不會太難,所以在大部分的情況下,這兩者都還算夠用。
這兩者最大的問題在於,他們都在主執行緒上,所以主執行緒有很多任務的時候,他們會有一點延遲。反過來說,如果他們執行的很頻繁的話,也會反過來阻塞主執行緒,造成載入速度變慢或是 lag。另外這兩者幾乎沒辦法在背景執行,切換到其他分頁沒多久就會被瀏覽器自動節流。
這兩個問題加起來一起看的話,如果因為專案性質或設計師的要求,很講究動畫的準確度的話,那 setInterval & setTimeout 不會是最好的方案。但還是要強調一下,這種情況相對少見,大部分的情況這兩者就很夠用了。反過來說,通常動畫延遲或不精確的問題,不會出現在 timer 或前端程式碼這一層,通常是其他層面的問題,例如資源太多、API 回應時間太長、使用者裝置或網路太差等等,不過這又是另一個話題了。
rAF 基本上是專門給動畫用的 timer,雖然也要用遞迴去寫,但跟 setTimerout 的原理一樣,難度應該不會太高。而且他雖然也在主執行緒上,但每次畫面更新之前都一定會跑一次,所以比較不會被延遲。反過來說,如果畫面沒有更新,就不會被執行,所以還是有一點背景執行的問題。
但相對來說,很多瀏覽器會對 rAF 與動畫的渲染做特別的優化,因為他就是預設專門給動畫渲染用的,所以跟動畫或畫面相關的 timer,用 rAF 一定是最佳解,請不要自作聰明考慮其他方法,這也是為什麼 Low-level API 會用 rAF 去做 render loop。
timer 的問題 87% 是出現在準確度與背景執行,這兩個問題的根本原因在於,JavaScript 是單執行緒的語言,而 setInterval, setTimeout, rAF 都執行在主執行緒上。照這個推論來看,用 worker 開另一條執行緒,再搭配簡單直覺的 setInterval,應該就會是最佳解。
當然 worker 最大的問題在於,跟動畫或 DOM 操作有關的資料,不一定能傳到 worker 裡面。此外因為是另一條主執行緒,所以就算能跟主執行緒互相傳遞資料,語法上還是會稍微複雜一點,很多邏輯跟平常的寫法不太一樣,要稍微適應一下。而且因為是另一條執行緒,所以就算是背景執行,瀏覽器有時候還是會對 worker……網開一面🥹🥹不過這部分各個瀏覽器的實作不一定一致,甚至各個版本也不一定一致,所以當參考就好。
所以結論上來說,原則上跟動畫或渲染相關的任務,因為 worker 先天上有限制,再加上 rAF 有特別優化,所以用 rAF 是最好的。但除此之外,如果有需要對 timer 做最佳化的話,那 worker 搭配一般的 setInterval 是最佳解,例如 polling 就非常適合用 worker,甚至有一派說法是說,所有跟 state 有關的處理都應該寫成 worker,比較節省效能。
就我的經驗來說,這種執行緒阻塞的問題,通常是結果而不是原因,就跟後端的 cuncurrency 一樣,問題很少真的出現在這一層。所以沒錯,把 setInterval 改成 rAF 甚至是 worker,通常會有顯著的效能提升,但這有點算是寅吃卯糧,跟 B 層次借錢處理 A 層次的問題,挖東牆補西牆,不一定是最佳解。大部分的時候,簡單的 setInterval 就非常夠用了。