优化实战 第 16 期 - 合理利用渲染帧内的空闲时间

1,193 阅读2分钟

事件处理和屏幕更新是用户关注性能最明显的两种方式,防止事件队列中出现卡顿是很重要的。可以充分利用浏览器渲染帧的空余时间,既不会导致系统延迟,也有助于浏览器的事件循环平稳运行

浏览器的 Event Loop

EventLoop.png

同步任务

在主线程上排队执行的任务,只有当前任务执行完毕,才会执行下一个任务

异步任务

不进入主线程,而是进入 “任务队列” 的任务,当主线程空闲的时候去读取执行

宏任务(macro):setInterval()setTimeout()fetchXMLHttpRequest

微任务(micro):MutationObserverpromise.then()

注意:微任务会优先宏任务执行,只有微任务队列空了才会去执行下一个宏任务;new Promise 进入主线程中立刻执行,而 promise.then() 则属于微任务

浏览器的渲染帧

FRAME.png

浏览器的事件循环 Event Loop 也是浏览器渲染帧里的一部分

当前渲染帧所花费的时间小于 16.67ms 的时候,浏览器会产生空闲时间,即 rIC

至于 16.67ms 是怎么得出来的,可阅读 第15期 - 巧用浏览器的页面绘制周期 一文

幕后任务协作调度 API

  • 利用浏览器渲染帧内的空闲时间

    const handle = window.requestIdleCallback(callback, { 
      timeout: 2000 
    })
    const callback = ({didTimeout, timeRemaining} = deadline) => {
      // 当前帧有剩余时间或者超时的时候
      while(timeRemaining() > 0 || didTimeout) {
        // 指定的任务
      }
    }
    

    如果一直没有空闲时间,在超过 timeout 设置的时间后,则会强制执行

  • 优雅降级

    window.requestIdleCallback = window.requestIdleCallback || function(handler) {
      let startTime = Date.now()
      return setTimeout(() => {
        handler({
          didTimeout: false,
          timeRemaining: () => Math.max(0, 50.0 - (Date.now() - startTime))
        })
      }, 1)
    }
    
    window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
      clearTimeout(id);
    }
    

    回退到 setTimeout(),限制每次传递的运行时间不超过 50 毫秒,来兼容不支持后台任务 API 的浏览器

注意事项

  • 避免在空闲回调中执行占用时间不可预测的任务

    事件循环有可能会用尽所有的可用时间,虽然设置 timeout 可以保证代码按时执行,但是在剩余时间不足以强制执行代码的时候,就会造成页面卡顿或者动画不流畅

  • 避免在空闲回调中操作DOM

    空闲回调执行的时候,当前帧已经结束绘制,所有布局的更新和计算也已经完成。如果你需要在回调中改变 DOM,应该使用 window.requestAnimationFrame() 来调度它

    一起学习,加群交流看 沸点