requestIdleCallback — 善用空閑時間

愷開
De-Magazine
Published in
6 min readMar 15, 2019

許多網頁當中都有各式各樣的 script 需要執行,也會有優先程度,像是比較重要的:渲染 UI,註冊相關的互動事件,呼叫 API 拿取資料等等是高優先的任務,而像是比較不重要的任務有:Analytic (視使用場景而定)、lazy loading、初始化不重要的事件。

怎樣才算 Idle?

怎樣才能知道瀏覽器處於 Idle 的狀態?這是一個相當複雜的問題,瀏覽器幫我們排程了一連串的任務,解析 HTML, CSS, Javascript、渲染 UI、API calls、抓取圖片並且解析、GPU 加速等等,要知道什麼時候閒置勢必得了解瀏覽器的排程工作。不過很幸運地是,requestIdleCallback 幫我們解決了這個問題。

requestIdleCallback 簡介

requestIdleCallback 會在 frame 的最後執行,但並不是每一個 frame 都保證會執行 requestIdleCallback。這個原因很簡單,我們無法保證每一個 frame 結束時我們還有時間,所以並不能保證 requestIdleCallback 的執行時間。

仔細一看,會覺得 requestIdleCallback 有點像是 context switch 的感覺,你可以在 frame 與 frame 之間完成一些工作,中斷一下,然後再繼續執行。

requestIdleCallback(fn, {timeout})

另外第二個參數 options 裡頭有個 timeout 選項,如果在 timeout 期間瀏覽器都還沒有呼叫的話,你可以用這個 timeout 讓瀏覽器停下手邊的工作強制呼叫。

這不是一個適當的使用方式,因為我們使用 idle 的原因就是因為這個任務是不重要的,沒必要為了它而打斷手邊的工作。瀏覽器提供了這份彈性給我們,有時你還是會希望事件在某個時間點以內觸發。

cancelIdleCallback(id)

對應到 requestIdleCallback() 會回傳一個 id,我們也可以呼叫 cancelIdleCallback(id) 來取消不需要的 idle callback。

requestIdleCallback 會不會被中斷?

deadline 參數表示這一個 frame 當中,你有多少時間可以完成這個任務,傳入的 callback 可以取得這個參數,根據官方的說法,就算超過了這個時間,瀏覽器也不會強制中斷你的任務,只是希望你能夠在 deadline 完成,讓使用者有最佳體驗。

deadline.timeRemaining() 回傳當前的可利用的時間還有多少。

在 requestIdleCallback 執行 DOM 操作會怎樣?

不妨設想一下,前文有提到,requestIdleCallback 會在 frame 的最後才執行,表示瀏覽器已經做完 recaculate, layout, paint 的工作了,在這時修改 DOM 的話,等於強迫瀏覽器又要再一次做 recaculate style, layout, paint 的排程。

在 requestIdleCallback 裡呼叫 requestIdleCallback 會怎樣?

requestIdleCallback 裡頭呼叫 requestIdleCallback 是合法的。不過這個 callback 會被安排到下一個 frame。(實際上並不一定是下一個 frame,視瀏覽器的排程而定)

Example, please!

假設我們有幾個使用者,當滑鼠移至大頭照上方時,會出現個人簡介。為了善用 idle 期間,我們可以在 requestIdleCallback 就先偷偷抓取需要的 API,如果都還沒有 fetch 過資料,再呼叫 API 去拿資料。

這個例子當中,我們利用 requestIdleCallback 先去抓取 user 的資料存起來,之後若使用者點擊大頭照就可以直接秀出來。如果還沒有抓到的話再重新 fetchUser 一次。

為了示範 requestIdleCallback 的效果,這個例子看起來挺麻煩的,不但需要維護一組 queue 來判斷是否已經 fetch 過,如果 mouseover 觸發了,還要另外判斷一次 userIntro 是否有值,開發上反而變得比較麻煩一些,一次抓取多位使用者的資料還比較省事一些,不過這完全根據需求與使用場景而定。

mouseover the user avatar

Analytic

另外一個常見的場景是做追蹤,例如追蹤使用者點擊按鈕,點擊播放,觀看時間等等。我們可以先蒐集事件,再利用 requestIdleCallback 的方式一次送出。像是:

這裡加上了 timeout 來確保瀏覽器會呼叫 schedule 函數。

Lazy Loading

透過 requestIdleCallback ,我們可以初始化一些載入時間比較長的物件,像是這篇文章當中,有提到利用 requestIdleCallback 來實作 IdleValue ,用來初始化像是 Intl.DateFormat 的物件。

How about React?

為了避免其他不必要的工作阻斷 main thread,我們可以整合一下 react hooks,將不必要的工作全部丟進去 useIdleCallback 裡頭。

結論

瀏覽器支援度還不算太好:

螢幕快照 2019-03-15 上午10.42.06

以上這些例子只是為了示範方便,實際上要處理的問題很多,例如如何管理 queue、如何管理 timeout,甚至幫你的任務標註優先度以確保執行順序等等,都是實際應用時需要考慮的地方。

在 react 新實作的 Fiber 當中也是利用類似的機制來完成,過去執行 work 的時候,可能會因為 tree 的深度太深,或是 updateQueue 太多,導致 main thread 卡住,更新時間過長。Fiber 的機制則是把工作拆成一小塊,並且依照優如果有其他優先度高的 work 就先過去做,再回來繼續處理其他 work。

參考文章:

--

--

愷開
De-Magazine

(Medium 不再更新文章) https://blog.kalan.dev 軟體工程師 / Working in Fukuoka, Japan。 平時喜歡用程式探索各種可能性,用網頁說故事、創造工具,邁向更好的生活。