前端要對使用者的電池負責!

語言: CN / TW / HK

theme: simplicity-green

highlight: a11y-dark


前言

我有一個壞習慣:就是下班之後不關電腦,但是電腦一般來說第二天電量不會有什麼損失。但是後來突然有一天它開不起來了,上午要罷工?那也不好吧,也不能在光天化日之下划水啊;聰明的我還是找到了原因:電池耗盡了;於是趕緊來查一查原因,到底是什麼程式把電池耗盡了呢?

這裡我直接揭曉答案:是一個Web應用,看來這個鍋必須要前端來背了;我們來梳理一下js中有哪些耗電的操作。

js中有哪些耗電的操作

js中有哪些耗電的操作?我們不如問問瀏覽器有哪些耗電的操作,瀏覽器在渲染一個頁面的時候經歷了GPU程序、渲染程序、網路程序,由此可見持續的GPU繪製、持續的網路請求和頁面重新整理都會導致持續耗電,那麼對應到js中有哪些操作呢?

Ajax、Fetch等網路請求

單只是一個AjaxFetch請求不會消耗多少電量,但是如果有一個定時器一直輪詢地向伺服器傳送請求,那麼CPU一直持續運轉,並且網路程序持續工作,它甚至有可能會阻止電腦進行休眠,就這樣它會一直輪詢到電腦電池不足而關機; 所以我們應該儘量少地去做輪詢查詢;

持續的動畫

持續的動畫會不斷地觸發GPU重新渲染,如果沒有進行優化的話,甚至會導致主執行緒不斷地進行重排重繪等操作,這樣會加速電池的消耗,但是js動畫與css動畫又不相同,這裡我們埋下伏筆,等到後文再講;

定時器

持續的定時器也可能會喚醒CPU,從而導致電池消耗加速;

上面都是一些加速電池消耗的操作,其實大部分場景都是由於定時器導致的電池消耗,那麼下面來看看怎麼優化定時器的場景;

針對定時器的優化

先試一試定時器,當我們關閉螢幕時,看定時器回撥是否還會執行呢? js let num = 0; let timer = null; function poll() { clearTimeout(timer); timer = setTimeout(()=>{ console.log("測試後臺時是否列印",num++); poll(); },1000*10) } 結果如下圖,即使暫時關閉螢幕,它仍然會不斷地執行:

未命名.png

如果把定時器換成requestAnimationFrame呢? js let num = 0; let lastCallTime = 0; function poll() { requestAnimationFrame(() =>{ const now = Date.now(); if(now - lastCallTime > 1000*10){ console.log("測試raf後臺時是否列印",num++); lastCallTime = now; } poll(); }); } 螢幕關閉之前列印到了1,螢幕喚醒之後才開始列印2,真神奇!

未命名.png

當螢幕關閉時回撥執行停止了,而且當喚醒螢幕時又繼續執行。螢幕關閉時我們不斷地去輪詢請求,重新整理頁面,執行動畫,有什麼意義呢?因此才出現了requestAnimationFrame這個API,瀏覽器對它進行了優化,用它來代替定時器能減少很多不必要的能耗;

requestAnimationFrame的好處還不止這一點:它還能節省CPU,只要當頁面處於未啟用狀態,它就會停止執行,包括螢幕關閉、標籤頁切換;對於防抖節流函式,由於頻繁觸發的回撥即使執行次數再多,它的結果在一幀的時間內也只會更新一次,因此不如在一幀的時間內只觸發一次;它還能優化DOM更新,將多次的重排放在一次完成,提高DOM更新的效能;

但是如果瀏覽器不支援,我們必須用定時器,那該怎麼辦呢?

這個時候可以監聽頁面是否被隱藏,如果隱藏了那麼清除定時器,如果重新展示出來再建立定時器: ``` let num = 0; let timer = null; function poll() { clearTimeout(timer); timer = setTimeout(()=>{ console.log("測試後臺時是否列印",num++); poll(); },1000*10) }

document.addEventListener('visibilitychange',()=>{ if(document.visibilityState==="visible"){ console.log("visible"); poll(); } else { clearTimeout(timer); } }) ```

針對動畫優化

首先動畫有js動畫、css動畫,它們有什麼區別呢?

開啟《我的世界》這款遊戲官網,可以看到有一個keyframes定義的動畫:

未命名.png

切換到其他標籤頁再切回來,這個掃描二維碼的線會突然跳轉;

因此可以得出一個結論:css動畫在螢幕隱藏時仍然會執行,但是這一點我們不好控制。

js動畫又分為三種種:canvas動畫、SVG動畫、使用js直接操作css的動畫,我們今天不討論SVG動畫,先來看一看canvas動畫(MuMu官網):

未命名.png

canvas動畫在頁面切換之後再切回來能夠完美銜接,看起來動畫在頁面隱藏時也並沒有執行;

那麼js直接操作css的動畫呢?動畫還是按照原來的方式一直執行,例如大話西遊這個官網的”獲獎公示“;針對這種情況我們可以將動畫放在requestAnimationFrame中執行,這樣就能在使用者離開螢幕時停止動畫執行

上面我們對大部分情況已經進行優化,那麼其他情況我們沒辦法考慮周到,所以可以考慮判斷當前使用者電池電量來相容;

Battery Status API兜底

瀏覽器給我們提供了獲取電池電量的API,我們可以用上去,先看看怎麼用這個API:

呼叫navigator.getBattery方法,該方法返回一個promise,在這個promise中返回了一個電池物件,我們可以監聽電池剩餘量、電池是否在充電; ```js navigator.getBattery().then((battery) => { function updateAllBatteryInfo() { updateChargeInfo(); updateLevelInfo(); updateChargingInfo(); updateDischargingInfo(); } updateAllBatteryInfo();

battery.addEventListener("chargingchange", () => { updateChargeInfo(); }); function updateChargeInfo() { console.log(Battery charging? ${battery.charging ? "Yes" : "No"}); }

battery.addEventListener("levelchange", () => { updateLevelInfo(); }); function updateLevelInfo() { console.log(Battery level: ${battery.level * 100}%); }

battery.addEventListener("chargingtimechange", () => { updateChargingInfo(); }); function updateChargingInfo() { console.log(Battery charging time: ${battery.chargingTime} seconds); }

battery.addEventListener("dischargingtimechange", () => { updateDischargingInfo(); }); function updateDischargingInfo() { console.log(Battery discharging time: ${battery.dischargingTime} seconds); } });

``` 當電池處於充電狀態,那麼我們就什麼也不做;當電池不在充電狀態並且電池電量已經到達一個危險的值得時候我們需要暫時取消我們的輪詢,等到電池開始充電我們再恢復操作

後記

電池如果經常放電到0,這會影響電池的使用壽命,我就是親身經歷者;由於Web應用的輪詢,又沒有充電,於是一晚上就耗完了所有的電,等到再次使用時電池使用時間下降了好多,真是一次痛心的體驗;於是後來我每次下班前將Chrome關閉,並關閉所有聊天軟體,但是這樣做很不方便,而且很有可能遺忘;

如果每一個前端都能關注到自己的Web程式對於使用者電池的影響,然後嘗試從定時器和動畫這兩個方面去優化自己的程式碼,那麼我想應該就不會發生這種事情了。

參考: MDN