什么是卡顿
我们的产品或者页面大部分情况下通过视觉变化给予用户反馈,这些视觉变化通过浏览器内核的UI线程渲染出来, 为此我们认为当浏览器UI线程出现阻塞时,用户就会感觉到卡顿,RAIL性能模型将这个值设定在了50ms, 即当浏览器内核UI线程卡顿时间超过50ms则记一次卡顿异常。
从用户感官的角度来看,用户能感受到的延迟阈值是100ms,这里之所以设置成50ms是浏览器的事件队列机制决定的, 如果任意一次任务的耗时超过50ms,那么下一次用户输入发生时, 浏览器首先要等待上一个任务执行完成再进行下一个任务,为了能够在100ms内响应用户操作, 留给这一次操作响应的时间就不到50ms了,所以应用必须在每 50 毫秒内将控制返回给主线程.
可以从下图看出空闲任务如何影响卡顿:
为什么是100ms
下面从用户对性能延迟的感知来分析下
-
0至16毫秒
用户非常擅长跟踪运动,并且在动画不流畅时不喜欢它。 只要每秒渲染60帧(每帧16毫秒),他们就能感觉到动画的流畅性。而应用程序大约需要10毫秒才能生成一帧。
-
0至100毫秒
在此时间范围内相应用户的操作,用户会感觉到结果是非常及时的,如果时间再长,用户就会感觉到卡顿了。
-
100至1000毫秒
在此时间内,对于网络上的大多数用户而言,该任务是自然连续不断发展的,比如加载页面或更改视图。
-
1000毫秒以上
超过1000毫秒(1秒)后,用户将失去对执行任务的关注。
-
10000毫秒以上 超过10000毫秒(10秒)
用户会感到沮丧,并且很可能放弃任务。他们可能会或可能不会稍后再回来。
卡顿可能带来的体验问题
-
导致可交互时间延迟
页面加载时,Long Task占用主线程,导致用户无法和页面进行交互,即使页面已经渲染出来了
-
输入响应延迟
重要的用户交互事件(tap、click、scroll、wheel等)排在Long Task之后,造成糟糕的不可预知的用户体验问题
-
事件处理延迟
和输入延迟类似,但这里指处理事件回调的延迟,例如onload等事件,最终导致应用更新延迟
-
糟糕的动画和滚动体验
一些动画和滚动需要排版和主线程协同,一旦主线程被Long Task阻塞,就会影响动画和滚动的流畅性
如何在浏览器端查看卡顿
Chrome浏览器Performance里可以看到。红色代表任务超过50ms。
Long tasks
我们知道,js 是单线程的,js 用事件循环的方式来处理各个事件。当用户有输入时,触发相应的事件,浏览器将相应的任务放入事件循环队列中。js 单线程逐个处理事件循环队列中的任务。 如果有一个任务需要消耗特别长的时间,那么队列中的其他任务将被阻塞。同时,js 线程和 ui 渲染线程是互斥的,也就是说,如果 js 在执行,那么 ui 渲染就被阻塞了。此时,用户在使用时将会感受到卡顿和闪烁,这是当前 web 页面不好的用户体验的主要来源。 Lonag tasks API 认为一个任务如果超过了 50ms 那么可能是有问题的,它会将这些任务展示给应用开发者。选择 50ms 是因为这样才能满足RAIL 模型 中用户响应要在 100ms 内的要求。
如何做成报表
借助Long Task API来实现。
如何优化让用户感觉不到卡顿
-
requestAnimationFrame
它的作用就是将传入的 callback 在下一帧开始时立即执行。 可以通过这个API来让任务变成高优先级任务执行,对用户来说,UI相关就是高优先级任务。
-
requestIdleCallback
它的作用是将在浏览器的空闲时段内调用的函数排队。 这使开发者能够在主事件循环上执行后台和低优先级工作, 而不会影响延迟关键事件,如动画和输入响应。 函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
通过这个API可以使得低优先级的任务可以让出资源来供高优先级的任务和浏览器UI渲染先行,如果这些任务耗尽了这帧的时间,那低优先级的任务就会被排到下次帧空闲的时候执行
-
减少js中css Layout操作,减少重排和重绘。
比如改变width、height等。 获取一个元素的样式(getComputedStyle)时,也会触发layout 能够使用transform满足要求的就别使用position/width/height做动画。
-
减少DOM结构
当DOM结构越复杂时,需要重绘的元素也就越多,所以dom应该保持简单。特别是动画场景,或者要监听scroll/mousemove事件的。 另外使用flex比使用float在重绘方面会有优势,详见:《Avoid Large, Complex Layouts and Layout Thrashing》
React 最新的 Fiber 架构。很大方面为了解决 js 代码在执行过程中的 Long tasks 问题。 reconciliation (协调器) 是 React 用于 diff 虚拟 dom 树并决定哪一部分需要更新的算法。 协调器在不同的渲染平台是可以共用的(web, native)。react 之前的设计中,是一次性计算完子树的更新结果,然后立刻重新渲染出来。这样就很容易造成 Long tasks 问题。
Fiber 架构(16版本)使用了requestAnimationFrame、requestIdleCallback来解决这个问题,Fiber 的核心就是把长任务拆成多个短任务,并分配有不同的优先级, 然后对这些任务进行调度执行,从而达将重要内容先渲染并且不阻塞 gui 渲染线程的目的,从而解决了Long tasks的问题。 react 最新的17版本的Fiber架构是自己实现了requestAnimationFrame和requestIdleCallback两个函数,以求达到更精细的级别。