使用js在桌面上写一个倒计时器_论一个倒计时器的性能优化之路
原文發表于 2018.05.25,搬運自個人博客。
引子
回顧這半年,扛需求能力越來越強,業務代碼也是越寫越多。但稍一認真看看這些當時為了滿足快速上線所碼的東西,問題其實還是不少。這次就從一個簡單的計時器說起。
現狀
問題很明顯
倒計時器組件在一個活動列表頁面里被使用,列表中每一項都是一個促銷活動入口。倒計時器位于每個活動區塊的左上方,提醒用戶該活動還有多久結束,如下動圖所示(測試設備 SONY E5663,后同)。
當頁面滑動時,可以明顯看到計時器停止,這意味著頁面并沒有刷新。直到松手后一兩秒才恢復計時,且不穩定,又卡頓了一到兩秒。
如此明顯的問題嚇得筆者趕緊去后臺查閱了該頁面 PV 和 UV 數據,雖說不多,但還是有一批忠實用戶每天訪問,這可怎么對得起我們的衣食父母…!即便測試用的設備性能羸弱,更換 Chrome 模擬器以及 17 年的安卓旗艦機再次測試并未出現如此卡頓現象,但我們無法挑選客戶使用的設備,只能從技術角度解決問題,盡量提升用戶體驗。BTW,這臺 SONY 測試機就是由東南亞的業務方同學提供,應該是當地用戶的常用機型之一。
打臉與自我打臉
倒計時器組件的更新邏輯抽象如下,簡單概括就是使用 setInterval 定時更新 React 組件的狀態以實現倒數時間的更新:
不得不說,貼出這么一段槽點滿滿的代碼是極其需要勇氣的,這… 居然是我寫的?
那么開始分(tu)析(cao)吧,讓我們自上而下依次盤點:
順著這個思路,趕緊來改代碼吧!
提升更新效率
更新速度有多慢?
首先花幾秒鐘把這段代碼挪到 componentDidMount() 鉤子里。
接下來,既然頁面在 MBP 的 Chrome 模擬器上訪問沒有問題,那么可以做個簡單的對比實驗,看看手機與筆記本模擬器的性能差距。使用 performance.now 測量更新一次所花費的時間,示例代碼如下:
從下方兩張截圖可以看到測試機與模擬器的性能相差十倍左右,且測試機的運算時間波動較大(下方上圖為模擬器數據,下圖為測試機數據):
其實上面的埋點代碼添加在 setState 的回調函數里,就明顯能說明一個問題:setState 方法并不保證同步渲染更新,盡管截圖中的時序看上去是同步的。
重點是,整個更新渲染的周期非常長,即使降低至 30Hz 的流暢畫面要求,一幀可用的渲染時間也只有不到 34 毫秒,還不是業務代碼獨享! 之所以渲染速度慢,是因為調用一次 setState 方法會依次執行 React 生命周期中的 4 個函數:shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate (如下圖所示)。
Source: https://bit.ly/2Pb6sn5直接擼 DOM,要啥 jQuery
為了性能,這里采用最為簡單粗暴的方法,直接更新 DOM 節點的 HTML 值:
讓我們來看看效果如何:模擬器上的更新時間縮短至 0.3 毫秒,比之前快了十幾到二十幾倍;測試機的數據也漂亮多了(如下圖),再滑幾下試試… 美滋滋!
更好的更新策略
定時器最重要的功能就是確保時間準確,如果時間都不準了,那也就該洗洗睡了。除去與服務端同步校時之類的方案,還是繼續討論如何在 Web 前端領域力求計時準確。
并不精準的 setInterval
在修復前文提到的 setState 缺陷之后,最明顯的問題莫過于 setInterval 的使用。寫一個定時任務,不少小伙伴第一反應想到的也是 setTimeout 和 setInterval 函數,但是它們真的足夠精確嗎?這就要從 JS 的任務隊列及微任務隊列(也有稱 macrotask queue 和 microtask queue)說起了…
咳咳,我們言簡意賅總結下:JS 主線程執行時有一個棧存儲運行時的函數相關變量,遇到函數時會先入棧執行完后再出棧(廢話)。當遇到 setTimeout setInterval requestAnimationFrame 以及 I/O 操作時,這些函數會立刻返回一個值(如 setInterval 返回一個 intervalID )保證主線程繼續執行,而異步操作則由瀏覽器的其它線程維護。當異步操作完成時,瀏覽器會將其回調函數插入主線程的任務隊列中,當主線程執行完當前棧的邏輯后,才會依次執行任務隊列中的任務。
但是在每個任務之間,還有一個微任務隊列的存在。在當前任務執行完后,將先執行微任務隊列中的所有任務,例如 Promise process.nextTick 等操作。也就是說當 setInterval(fn, 1000) 等待 1 秒鐘后,fn 函數會被插入任務隊列中,但并不一定會立刻執行,還需要等待當前任務以及微任務隊列中的所有任務執行完。長此以往,使用 setInterval 的計時器超時將越來越嚴重。
如果有毅力的朋友推薦看看權威的 HTML 標準文檔,沒耐心的就看看這個動圖簡單感受一下原理吧。
所以回歸正題,不用 setInterval 那用啥?
天王蓋地虎,我有 rAF
解鈴還須系鈴人,既然我們的代碼執行時間在主線程中無法得到保證,那么還是要從更高抽象層級的瀏覽器中尋求辦法。好在目前主流瀏覽器都已提供一個在重繪前執行動畫相關函數的接口 requestAnimationFrame,用來更新計時器再合適不過。改造如下:
那么這樣實現足夠精準了嗎?打印出每次更新的時間戳瞅瞅(上圖為模擬器數據,下圖為測試機數據)。
可以看到模擬器上已經相當精準,每秒的誤差在 +0.15 毫秒左右,也就是運行將近 2 小時會有 1 秒的誤差,筆者覺得完全可以接受。不過測試機上的誤差就有點大了,每秒的誤差在 +10 毫秒左右,雖然筆者覺得也可以接受(很少有人會在活動頁停留很久),但本著工(tai)匠(gang)精神,想想是否還能優化呢?
正向反饋拯救采樣頻率
好奇心使筆者打印出了測試機調用 rAF 的時間間隔,絕大多數間隔在 16.6 毫秒左右,意味著手機 webview 也是 60Hz 的刷新頻率;不過也存在少數間隔時間遠超正常刷新時間,達到了 30 ~ 70 毫秒,如果觸發滑動操作可能會超過 100 毫秒。不得不說,測試機就要挑這么爛的 Orz…
仔細想想,測試機上的計時誤差本質是采樣頻率并未一直滿足 60Hz,當某一次采樣時間超過 16.6 毫秒且剛好需要刷新動畫時,就會產生誤差。同時每次誤差都是超時而非提前,這樣就在延時的道路上越走越遠了。
那么反向思考,每當觸發更新事件時,超時時段(超過 1 秒的時間)是已知的。如果將其補償到下一次計時中,應該能減緩誤差的擴大速度。代碼如下:
觀察測試手機打印的時間,發現此法完全是可行的。每當超時間隔超過正常的刷新頻率 16.6 毫秒時,相當于趕上了下一次采樣窗口的伊始,因此會被校正。相比手機上每隔兩三秒校正一次,PC 模擬器的采樣時間變化顯得尤為明顯。
Reference
- Tasks, microtasks, queues and schedules
- How does a single thread handle asynchronous code in JavaScript?
- HTML Living Standard — Last Updated 25 May 2018
- Window.requestAnimationFrame()
- 本文作者: John Chou
- 本文鏈接: https://blog.joouis.com/2018/05/25/optimization-road-of-count-down-timer/
- 版權聲明: 本博客所有文章除特別聲明外,均采用 BY-ND 許可協議。轉載請注明出處!
相關文章
- Javascript 簡潔之道:如何使用類重構
- JavaScript 性能優化概觀
- Weex Android 發車指南(已棄車)
- 十分鐘帶你了解國產自制開源插件 structure-view
- 小議 Javascript 數組去重
總結
以上是生活随笔為你收集整理的使用js在桌面上写一个倒计时器_论一个倒计时器的性能优化之路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网页复选框设置只能选一个_男生在密室呆一
- 下一篇: 电脑内存占用莫名很高_CPU占用高,电脑