INP 是什么?
Interaction to Next Paint (INP)
INP是一项指标,通过观察用户在访问网页期间发生的所有点击、点按和键盘互动的延迟时间,评估网页对用户互动的总体响应情况。
互动是指在同一逻辑用户手势期间触发的一组事件处理脚本。例如,触摸屏设备上的“点按”互动包括多个事件,例如 pointerup、pointerdown 和 click。交互可由 JavaScript、CSS、内置浏览器控件(例如表单元素)或这些控件的组合促成。
互动的延迟时间包括驱动互动的一组事件处理脚本中最长的时长,即从用户开始互动到下一帧显示视觉反馈的那一刻。
INP 的目标是确保尽可能缩短从用户发起互动到下一帧绘制完成之间的时间,以尽可能缩短用户进行的所有互动或大多数互动。
注意 :Interaction to Next Paint (INP) 是一项待处理的核心 Web 指标指标,将于 2024 年 3 月 12 日取代 First Input Delay (FID)。INP 使用 Event Timing API 中的数据评估响应能力。如果互动导致页面无响应,用户体验会很糟糕。INP 会观察用户与网页进行的所有互动的延迟时间,并报告所有(或几乎所有)互动所低于的单个值。INP 较低意味着网页始终能够快速响应所有或绝大多数用户互动。
INP 得分是多少?
很难在响应能力指标上固定“良好”或“差”等标签。一方面,您需要鼓励优先考虑良好响应能力的开发实践。另一方面,您必须考虑这样一个事实,即人们用来设定实现预期开发期望的设备的功能有很大的差异。
为确保提供良好的响应速度的用户体验,
- INP 低于或等于 200 毫秒表示您的网页具有良好的响应速度。
- INP 高于 200 毫秒且低于或等于 500 毫秒表示网页的响应能力需要改进。
- INP 高于 500 毫秒表示您的网页响应速度很差。
优化由JavaScript导致的INP不佳
线上情况(chrome性能分析):
可以看到单个任务由超过600ms的情况,单个任务执行时间过长导致INP 高于 500 毫秒表示您的网页响应速度很差,需要优化耗时较长的任务。
通过分析得知长任务在执行发送曝光sendExp和updateComponent、updateChild等重渲染任务,“任务”是指浏览器执行的任何独立工作。任务涉及的工作包括渲染、解析 HTML 和 CSS、运行编写的 JavaScript 代码以及无法直接控制的其他事情。其中,程序员编写并部署到网络的 JavaScript 是主要任务来源。
如何优化耗时较长的任务,我们一般有以下几个方法:
1. 避免长任务
在国际往返一屏项目中,由于去程和返程来回横滑切换时需要切换航班卡片展示形态,在一些热门航线如北京往返香港,去程列表航班卡片数百个,返程航班卡片数百个,一次切换形态,props发送变化会造成大量的父子组件重新渲染,组件的重新渲染又会引起大量曝光请求重新发送,使得页面性能消耗巨大,严重时能造成页面崩溃。
props只要任何改变就会导致父子组件重渲染,有一些props发送改变时是不必要都去重渲染浪费性能,所以这里通过memo来缓存组件,只有必要内容发送改变时才进行重渲染
export default memo(FlightCard, (props, nextProps) => {
// 只对比需要涉及组件重渲染的props属性,不一味任何props的改变都无脑重渲染
const { isActive, moduleIndex, cardIndex, item = {}, taxLabel, firstClick } = props;
const {
isActive: nextIsActive,
moduleIndex: nextModuleIndex,
cardIndex: nextCardIndex,
item: nextItem = {},
taxLabel: nextTaxLabel,
firstClick: nextFirstClick,
} = nextProps;
const preProps = {
isActive,
moduleIndex,
cardIndex,
item,
taxLabel,
firstClick,
};
const compareProps = {
isActive: nextIsActive,
moduleIndex: nextModuleIndex,
cardIndex: nextCardIndex,
item: nextItem,
taxLabel: nextTaxLabel,
firstClick: nextFirstClick,
};
try {
// 重要信息相同则不重新渲染,避免性能过量消耗
if (isEqual(compareProps, preProps)) {
return true;
}
} catch (e) {
return false;
}
return false;
});
2. 拆分长任务,请勿阻塞主线程
线上版本中,无论是上下滑动还是左右横滑,都会发送大量曝光请求,当频繁滑动的时候,网络请求列表中同时发送的请求数量过大,导致页面卡顿甚至崩溃,避免大量曝光请求同步发送,本着不阻塞主线程、拆分长任务的原则,采用防抖debounce + 分批处理曝光请求的思想:
- 每次需要曝光的元素出现在视口,不是立马发送曝光请求而是将曝光信息添加到一个全局变量数组中,执行防抖函数进行发送曝光信息,只有到当用户停止新增曝光埋点一段时间之后,再统一发送收集到的曝光信息。
- 分批处理曝光请求,设置每一批次发送固定数量的曝光信息,每发送一批数据延迟一段时间再继续发送后续批次,以免完全占用主线程导致用户操作回调排队过长
// 确保全局exposures数组存在 window.exposures = window.exposures || []; // 这个标志用于确保页面unload监听器只添加一次 window.isBeforeUnloadEventListenerAdded = window.isBeforeUnloadEventListenerAdded || false; // 确保全局防抖 window.sendExpTimeout = window.sendExpTimeout || null; // 发送全部曝光信息 const sendExposures = debounce(() => { // 发送window.exposures中的曝光数据 if (window && window.exposures && window.exposures.length > 0) { // 每发送100个曝光数据,等待1000 ms再发送,避免长期占用主线程 sendRequestsInBatches([...window.exposures], 100, _sendExp); } // 防抖500 ms,用户500ms内无新增曝光操作才发送 }, 500); // 批量发送曝光信息 async function sendRequestsInBatches(dataArray, batchSize, onRequest) { for (let i = 0; i < dataArray.length; i += batchSize) { // 获取当前批次的数据 const batch = dataArray.slice(i, i + batchSize); // 使用 Promise.all 并发发送请求 Promise.all(batch.map(dataItem => { onRequest(dataItem); })).then(() => { // 发送完这一批数据,在window.exposures中清除 window && window.exposures.splice(0, batchSize); }).catch(() => { console.error("exp信息发送失败"); }) // 可能需要延迟一小段时间(1000 ms)以避免完全占用网络和CPU await new Promise(resolve => setTimeout(resolve, 1000)); } } // 组件需要曝光 const sendExp = () => { window && window.exposures.push( { logkey: dataExpKey, exargs: expTrackInfo.trackArgs, spm: spm, scm: expTrackInfo.scm || '' } ) sendExposures(); }