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