前端要对用户的电池负责!

47,019 阅读6分钟

前言

我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢?

这里我直接揭晓答案:是一个Web应用,看来这个锅必须要前端来背了;我们来梳理一下js中有哪些耗电的操作。

js中有哪些耗电的操作

js中有哪些耗电的操作?我们不如问问浏览器有哪些耗电的操作,浏览器在渲染一个页面的时候经历了GPU进程、渲染进程、网络进程,由此可见持续的GPU绘制、持续的网络请求和页面刷新都会导致持续耗电,那么对应到js中有哪些操作呢?

Ajax、Fetch等网络请求

单只是一个AjaxFetch请求不会消耗多少电量,但是如果有一个定时器一直轮询地向服务器发送请求,那么CPU一直持续运转,并且网络进程持续工作,它甚至有可能会阻止电脑进行休眠,就这样它会一直轮询到电脑电池不足而关机; 所以我们应该尽量少地去做轮询查询;

持续的动画

持续的动画会不断地触发GPU重新渲染,如果没有进行优化的话,甚至会导致主线程不断地进行重排重绘等操作,这样会加速电池的消耗,但是js动画与css动画又不相同,这里我们埋下伏笔,等到后文再讲;

定时器

持续的定时器也可能会唤醒CPU,从而导致电池消耗加速;

上面都是一些加速电池消耗的操作,其实大部分场景都是由于定时器导致的电池消耗,那么下面来看看怎么优化定时器的场景;

针对定时器的优化

先试一试定时器,当我们关闭屏幕时,看定时器回调是否还会执行呢?

let num = 0;
let timer = null;
function poll() {
    clearTimeout(timer);
    timer = setTimeout(()=>{
        console.log("测试后台时是否打印",num++);
        poll();
    },1000*10)
}

结果如下图,即使暂时关闭屏幕,它仍然会不断地执行:

未命名.png

如果把定时器换成requestAnimationFrame呢?

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中返回了一个电池对象,我们可以监听电池剩余量、电池是否在充电;

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程序对于用户电池的影响,然后尝试从定时器和动画这两个方面去优化自己的代码,那么我想应该就不会发生这种事情了。注意:桌面端程序也有可能是披着羊皮的狼,就是披着原生程序的Web应用;

参考: MDN