解决ios省电模式下浏览器 requestAnimationFrame 锁30帧问题

2,037 阅读3分钟

如果只想看解决方法,直接跳转到第四小节

1 描述

ios设备上,不管是iPhone还是iPad,不知道从什么时候开始,只要开启省电模式,requestAimationFrame都会锁死 30 帧,甚至在支持 120 刷新率的 iPad Pro 上,不开省电模式,刷新率只能到60,开省电模式依然雷打不动30fps(手上只有老款pro,新款不知道什么情况),在搜索引擎上搜索的时候也搜索到了相关问题 的描述

这就会导致,使用 js 去操作dom实现一些手势的时候,就会出问题特别卡的情况,而安卓就不会出现这个问题

比如下面的例子,在Window/Android下都非常流畅,但 ios 省电模式下和废了一样

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>

  <div class="box" style="width:100px;height:100px;border: 1px solid #000"></div>

  <script type="module">

    const box = document.querySelector('.box')

    const o = {
      startX: 0,
      startY: 0,
      previousX: 0,
      previousY: 0,
      currentX: 0,
      currentY: 0,
    }

    function step() {
      if (o.previousX !== o.currentX || o.previousY !== o.currentY) {
        box.style.transform = `translate(${o.currentX - o.startX}px, ${o.currentY - o.startY}px)`
        o.previousX = o.currentX
        o.previousY = o.currentY
      }

      requestAnimationFrame(step)
    }

    box.addEventListener('touchstart', (e) => {
      e.preventDefault()
      o.startX = e.touches[0].clientX
      o.startY = e.touches[0].clientY
      box.style.transition = ''
      requestAnimationFrame(step)
    })

    box.addEventListener('touchmove', (e) => {
      e.preventDefault()
      o.currentX = e.touches[0].clientX
      o.currentY = e.touches[0].clientY
    })

    box.addEventListener('touchend', (e) => {
      box.style.transition = '.3s transform ease'
      box.style.transform = 'translate(0px, 0px)'
      o.startX = 0
      o.startY = 0
      o.previousX = 0
      o.previousY = 0
      o.currentX = 0
      o.currentY = 0
    })

  </script>
</body>

</html>

GIF 2021-11-15 11-07-13.gif

2 转机

在很长的时间下,这个问题都是无解的情况,我尝试去找一些大厂的h5页面,试图从他们的页面中找到解决方案,但是依然都是都是没什么结果

youtube列表上下滚动时的tab的收缩

GIF 2021-11-15 11-25-34.gif

微博图标拖动手势

GIF 2021-11-15 11-26-48.gif

百度百科轮播图

GIF 2021-11-15 11-24-21.gif

事情转机在最近,在百度搜索基金的时候,我发现,有一个横向列表,明明用的也是transform实现手势,但是在手机上开省电模式,发现竟然和关闭省电模式没什么区别

GIF 2021-11-15 11-35-18.gif

还有这种事情?难道基金和苹果有什么py交易,我突然想到,以前用了一个库,better-scroll,也使用了这种方式,于是,我立马跑了一个demo

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <style>
    .wrapper {
      width: 300px;
      height: 400px;
      overflow: hidden;
    }

    .content>div {
      height: 200px;
      background-color: #f5f5f5;
      border: 1px dashed #000;
    }
  </style>
  <script src="https://cdn.bootcdn.net/ajax/libs/better-scroll/2.4.1/better-scroll.js"></script>
</head>

<body>

  <div class="wrapper">
    <div class="content">
      <div>content1</div>
      <div>content2</div>
      <div>content3</div>
      <div>content4</div>
      <div>content5</div>
      <div>content6</div>
      <div>content7</div>
      <div>content8</div>
    </div>

    <script>
      new BetterScroll.default(document.querySelector('.wrapper'), { scrollY: true })
    </script>

</body>

</html>

神奇了,不知道 better-scroll 干了什么事情,better-scroll源码还挺大的,并不是很想看它的源码

但是巧了,之前为了体积问题,我也写过一个React组件,在安卓上模拟ios的弹性滚动的效果,并且在省电模式下,也能达到满帧的刷新率,运气太好了,原来答案一直就在身边,并且这个组件的代码才2,300行,看起来非常方便

GIF 2021-11-15 12-11-23.gif

并且神奇的是,浏览器执行到这个组件的时候,再切换到别的需要js手势的组件的时候,也能达到满帧的效果

3 查找关键代码

经过不断尝试,发现了一个规律,当存在惯性滚动的时候,就会把浏览器的刷新率改成60fps`

GIF 2021-11-15 12-11-23.gif

当不存在惯性滚动的时候,就还是30fps

GIF 2021-11-15 12-38-26.gif

所以,具体的修改帧率的逻辑一定是 touchend 触发之后触发惯性滚动的哪几行代码实现的

image-20211115124023236.png

最后,经过不断的删除代码与测试(两个小时过去), 最后只留下两行

box.style.transition = '...s transform'
box.style.transform = 'translate(...)'

不知道什么原因,只要存在一个盒子,在某一个时刻修改一下transitiontransform,并且这个元素要存在一定的宽度和高度,就能把浏览器的刷新率修改成60帧

4 解决问题

所以,在一个项目初始化的时候,只需要创建一个盒子,然后在下一次宏任务添加上transitiontransform。然后再过一会,把这个元素删除,就能解除safari30fps限制

主要解决代码如下

(function(){
    const b = document.createElement('div')
    b.style.pointerEvents = 'none'
    b.style.opacity = '0'
    b.style.width = '1800px'
    b.style.position = 'fixed'
    b.innerText = '.'
    document.body.appendChild(b)

    setTimeout(() => {
        b.style.transition = '1s transform'
        b.style.transform = 'translate(0)'

        setTimeout(() => {
            b.parentNode.removeChild(b)
        }, 100)

    })
})()

更详细的逻辑和的例子和代码我已经放到 github 上: github.com/bpuns/apple…

希望各位大佬能点一个star