这篇文章是我在公司做 INP 优化经验分享的演讲稿。
大家好,今天我要做的分享是关于 INP 的一些优化经验。
概念
首先,什么叫 INP 呢。 INP 的全称叫 Interaction to Next Pain ,翻译过来就是从交互到下一次绘制的延迟。这是 Google 提出来的一个衡量 Web 用户体验的指标。衡量一个 Web 页面体验和质量的指标有很多,为了帮助开发者专注于重要的用户体验指标,同时也为了降低学习成本,Google 提出了一套关键指标,叫 Core Web Vitals:
分别从加载性能、交互体验和视觉稳定性三个维度来衡量用户体验,其中 INP 就是衡量交互体验的指标。
在浏览器中,渲染引擎在每一帧都有机会渲染页面。假设你的显示器是 60 Hz,那么浏览器每秒可以渲染 60 次,每次渲染间隔时间大约就是 16.7ms。但是如果浏览器其他任务执行时间过长,导致迟迟无法执行渲染,就会给用户造成网页响应速度不够快的印象。
INP 就是算从用户触发交互到浏览器下一次绘制被阻塞的时间,如果说阻塞时间小于200ms,那么算得分良好。
这里要强调一下, INP 算的是浏览器下一次绘制的延迟,并不是算当前交互结果绘制的延迟。比如有些异步操作(调接口等),虽然等待时间较长,但是如果没有影响到浏览器的下一次绘制,那么不会影响 INP 结果。
一次交互可分为以下 3 个阶段:
-
输入延时(
Input Delay
)= 交互事件回调开始运行时 - 用户发起与页面的交互时 -
事件处理(
Processing Time
)= 事件回调运行完成时 - 事件回调运行开始时 -
渲染延时(
Presentation Delay
)= 浏览器显示包含交互的可视结果的下一帧渲染时 - 事件回调运行完成时
根据 Google 官方文档的说明, INP 的计算方式是:对于大多数网站(互动次数不超过 50 次)来说,用户和网页所有的互动中,延迟时间最长的互动会被报告为本次浏览的 INP ,然后系统会取所有网页浏览量的第 75 百分位作为网页的 INP。而对于有大量互动的网站,Google 会忽略每 50 次互动中最高的一次互动。
目前会被观察用于记录 INP 的互动类型有:
-
鼠标点击
-
触摸屏点按
-
实体键盘或屏幕键盘的按键
接下来我会从如何发现问题、如何定位问题以及如何解决问题三个阶段来阐述,我们在处理 INP 问题上的工作流程是怎样的。
如何发现问题
人为巡检
首先在前端团队内部,每周会有人对线上指标做定期巡检,大家每周都会收到一封关于核心指标状况报告的邮件,在邮件上汇总了各个业务线关键页面的核心指标状况,然后我们在邮件上就可以看到哪些页面的指标可能是不太健康的。
监控告警
其次在公司内部的监控系统看板上,我们也可以自行查阅具体页面的核心指标状态。然后 Sentry 也提供了监控 INP 的功能,而且还有针对 FID 的告警功能。 FID 叫 First Input Delay ,是一个记录第一次交互行为的指标,算作 INP 的前身,大家如果有需要的话可以自行配置。
如何定位问题
然后来讲一下就是当我们发现页面的 INP 指标偏高的时候,要怎么去定位到底是具体哪一个交互导致页面的 INP 值偏高。
Chrome 插件 Web Vitals
首先我们最常用到的是 Google 的一个插件叫 web vitals 。当我们在界面上进行一些交互操作的时候,比如我点了一个按钮,这个插件就会在控制台打印出该交互的 INP 值,还有交互分别在输入、事件处理和渲染阶段的耗时。
一般我们定位 INP 问题就是通过在界面上自行做一些交互操作,然后看看哪些交互操作的 INP 值是偏高的。
Chrome DevTools - Performance 面板
假设我们已经发现某个操作它的 INP 值偏高,那我们该怎么去分析原因呢?我们可以借助 Google 开发者工具的 Performance 面板来分析。
首先对一次交互进行录制,然后它会以堆栈的形式帮你列出来任务的执行形容,而且会根据耗时从高到低排列任务,在任务列表的右边还会标出任务所属文件,点开文件就可以看具体是哪一段代码执行耗时。
这里我还要补充一点,因为大部分开发者电脑性能配置都很好,但其实我们的用户他们的设备性能是非常不统一的,尤其是移动端会面临户外各种复杂网络环境的情况。 Performance 面板有 CPU 和 Network 两个选项,我们可以通过设置这两个选项来模拟用户的使用场景。
一般我们可以先设置 CPU 慢速的场景,看看 INP 值是否正常,如果说这时候 INP 指标正常,那么大概率是网络问题,然后我们再设置 Network 慢速的场景。
如何解决问题
接下来讲一下关于优化 INP 的一些思路。在最开始我们介绍过一次交互分为输入、任务执行和渲染三个阶段,我们优化的时候就是分别针对这三个阶段去做优化。
减少输入延迟
第一个输入阶段,我们要如何减少输入的延迟呢?
减少影响输入的长任务
如果当用户输入的时候有一个很长的任务,它占领了主线程,那么这个任务就会导致主线程的一个阻塞,然后从用户输入到触发时间回调的这个过程就会变得很长。所以说我们要尽量减少这种影响用户输入的长任务,我们可以把任务做一下切分,把它拆成更小颗粒度的任务。如果说一些任务它的执行优先级不那么高,我们可以放到后面再执行。
避免交互重叠
然后还有一个我们要做的,就是避免交互重叠。交互重叠的意思是说当用户完成了一个交互以后,本来浏览器要渲染这个交互的结果,但是这时候又产生了一个新的交互,这就叫交互重叠。之所以交互重叠对输入有影响是因为渲染第一次交互结果的时候会导致第二次交互的输入产生延迟。
这种情况我们可以通过防抖或者节流来控制用户触发输入事件的频率。
优化任务执行效率
然后来看看怎么优化任务执行效率。
优化代码逻辑
其实我们要优化任务执行效率,就是要对我们的代码逻辑做一个梳理,尽可能地减少冗余代码,还有一些不重要的任务我们可以放到下一个事件循环去执行,还有就是一些非常大型的任务,我们要对它做更小颗粒度的拆分。这里以下图的点击筛选查看结果为例:
这个交互简单来说就是用户点击了筛选弹窗右下角的“查看结果”按钮,然后弹窗会收起并在页面上展示筛选结果。
这个交互在优化前的代码执行流程是👇🏻这样的:
就是说我点了筛选项,然后前端会根据筛选项去调接口获取搜索结果。当用户点了筛选弹窗右下角的“查看结果”按钮,首先前端会记录弹窗内用户选中的筛选项数据,这样做的目的是为了让用户下次打开弹窗的时候会展示上一次选择的选项。然后就是关闭弹窗,然后再去行一个叫 updatePageData 的方法,这个方法主要就是更新页面数据,然后就是结束。
那么这一整个执行逻辑,它的执行过程有什么问题呢?
首先我们可以看到一整个执行流程它都是同步执行的。但其实有一些任务它的执行优先级并不那么高,它其实可以放到下一个事件循环去做。就比如“弹窗内记录筛选项数据“这一步,因为记录数据是为了下一次打开弹窗服务的,不一定马上就要做这件事,所以这一步其实可以放到下一个时间循环。
还有我们可以看到有一个很大的方法叫 updatePageData ,这个方法里面做了很多的事情,那为什么这个方法这么大呢?主要是因为这个方法被应用到了多个场景,比如页面初始化的时候会执行 updatePageData 方法、点击筛选项的时候也会执行 updatePageData 方法。所以这个方法相当于是把多个交互要执行的任务取了一个并集。但是对于单个交互比如我们举例的“查看结果”来说,其实 updatePageData 里的很多任务是不需要执行的。
那么我们就是要对这个大型的方法做一个更小颗粒度的拆分,然后根据具体的场景再对这些小任务做一个组合。
然后还有一个就是冗余问题。比如更新界面的时候会滚动到界面顶部,但如果页面本身就在顶部那么就没必要去操作 DOM 。
这个交互在优化后的代码执行流程是👇🏻这样的:
可以看到整个任务执行流程短了很多。
善用任务拆解API
然后呢我要讲一下,其实浏览器提供了很多可用于任务拆解的 API 。比如最常见的是 setTimeout ,它可以把某个任务放到下一个事件循环去执行,这样就可以把当前主线程让出来,去做一些更重要的事。
不过 setTimeout 本身的用意是延迟执行某些逻辑,它并不是专门用来做任务拆解的。浏览器提供了一些专门用来做任务拆解的 API ,比如 requestIdleCallback 、requestAnimationFrame 这些,还有最近新出的 scheduler.yidld 。
减少渲染延时
接下来我们讲讲如何减少渲染延时。
资源缓存
首先我们可以对一些静态资源做缓存,比如图片、图标这些做一下 CDN 缓存,这样就是尽快地去加载页面的一些内容。
尽早让用户得到交互反馈
还有就是我们要尽早地让用户得到交互反馈。比如展示日历弹窗的时候,可能有很多跟日期相关的计算逻辑,需要等到逻辑处理完毕才能展示日历。那么这个时候我们可以在打开弹窗的时候先让用户看到一个 loading 的样式,等逻辑处理完毕再展示日历。
还有一种情况就是某个子组件,它要展示的数据依赖于外部 props 的传入。比如用户在子组件内执行了某些操作产生了新的值,一般我们写代码的逻辑都是先在子组件内部调用 $emit
方法把新值传给父组件,然后父组件更新 props 把值再传给子组件,然后子组件再根据新值去渲染内容。
那么这个执行流程是很长的。我们可以在子组件内定义一个内部变量,当用户在子组件内执行操作产生新值的时候,我们可以先用内部变量在子组件内渲染新的内容,然后再去通知父组件更新 props 。因为对当前操作来说,最重要的是让用户看到他的操作结果,通知父组件更新这件事情更不重要。
<klk-date-picker :date="innerDate" @change="handleChange" />
<script>
export default class DateModal extends Vue {
@Prop({ type: Object, default: () => null }) date!: any
@Watch('date')
onDateChange(val) {
this.innerDate = val
}
handleChange(val) {
// 先更新交互结果,再通知父组件
this.innerDate = val
setTimeout(() => {
this.$emit('change', val)
})
}
}
</script>
避免渲染大型DOM
减少渲染延时还有一种方法是避免渲染一些大型 DOM 。就是我们要优先渲染在可见区域内的 DOM ,这里我们可以用到虚拟列表或者 CSS 的一个属性叫做 content-visibility
来延迟渲染可视区域外的 DOM 。
结语
最后,用户体验一直是前端开发者需要重点关注的领域,但是用户体验其实是一个比较抽象的概念,Web 关键指标就是去量化了这个抽象的概念。这样不仅降低了开发者的理解成本,而且也量化了前端开发者的工作产出。通过关键指标,我们知道了优化用户体验的一个方向。