深入理解 INP:从原理到实战的前端交互性能优化
背景
最近有做一些INP优化相关,简单记录一下。
INP高的重灾区就是手机端, 手机 CPU 弱、线程紧张,INP 高基本都出在移动端。
什么是 INP?
INP(Interaction to Next Paint,交互到下一次绘制) 是 Google Core Web Vitals 中评估网页交互响应性的核心指标。自 2024 年 3 月起,INP 正式取代了旧的 FID(First Input Delay),成为衡量用户交互体验的主要标准。
优化INP有什么用?
-
📱 体验层面:解决移动端点击卡顿、无响应,让交互秒反馈,大幅提升流畅度。
-
🔍 SEO 层面:INP 是 Google Core Web Vitals 核心指标,达标有利于搜索排名与流量。
-
💰 业务层面:交互更流畅,用户流失更少,留存、转化率更高。
-
🛠 技术层面:拆分长任务、优化主线程,让项目整体架构更轻、更快、更易维护。
INP vs FID:为什么要换?
| 对比维度 | FID (旧指标) | INP (新指标) |
|---|---|---|
| 测量范围 | 仅第一次交互 | 全生命周期所有交互 |
| 测量内容 | 仅输入延迟 | 输入 + 处理 + 渲染 全链路 |
| 真实性 | 片面 | 更全面、更真实 |
| 优化指导 | 只关注首次交互 | 关注所有交互场景 |
简单来说:FID 只是"开胃菜",INP 才是"全席宴"——它能真正反映用户在使用过程中的完整体验。
INP 测量的是什么?
INP 测量的是:用户进行一次有效交互(点击、点按、键盘输入)后,到浏览器完成下一次视觉更新(paint)所花费的总时间。
时序图解
用户交互(click/tap/keypress)
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐
│ 输入延迟 │ → │ 事件处理 │ → │ 样式计算 + 布局 + 绘制 │ → 下一帧 paint
└─────────────┘ └─────────────┘ └─────────────────────────┘
◀──────────────────── INP 完整耗时 ────────────────────────────▶
📌 关键点:INP 的最终值 = 页面生命周期中所有有效交互里最慢的那一次(取第 75 百分位)
评分标准(2025–2026)
根据 Google 官方标准,INP 以第 75 百分位(P75)进行评估:
| 等级 | 阈值 | 含义 |
|---|---|---|
| 🟢 良好 (Good) | ≤ 200 ms | 响应迅速,用户体验优秀 |
| 🟡 需改进 (Needs Improvement) | 200–500 ms | 有明显延迟感,需要优化 |
| 🔴 较差 (Poor) | > 500 ms | 严重卡顿,体验糟糕 |
优化目标:确保 75% 以上的用户交互响应时间控制在 200 ms 以内。
![]()
为什么 INP 会变差?
核心原因
主线程上存在长任务(Long Task > 50 ms),阻塞了浏览器处理后续交互和渲染。
典型场景
用户点击按钮
→ setLoading(true)
→ 立即执行 200–500 ms 的重计算/网络请求/复杂 DOM 操作
→ 用户迟迟看不到 loading 状态
→ 感觉卡顿、无响应
问题在于:setLoading(true) 虽然调用了,但由于主线程被长任务占用,浏览器根本没机会渲染这个状态变化!
![]()
三个阶段的问题分布
根据实际项目经验,三个阶段的问题分布大致如下:
-
输入延迟:30% - 通常是第三方脚本、初始化任务
-
事件处理:50% - 最常见的问题,业务逻辑复杂
-
呈现延迟:20% - DOM 操作、布局计算
INP 的三个阶段详解
INP 的测量范围涵盖三个关键阶段,每个阶段都可能成为性能瓶颈。深入理解这三个阶段,是优化 INP 的基础。
![]()
阶段一:输入延迟(Input Delay)
定义:从用户触发交互到事件处理器开始执行之间的等待时间。
原因:主线程被其他任务占用(长任务 > 50ms、同步渲染、第三方脚本、垃圾回收),浏览器无法立即响应交互。
案例:比如如下,输入延迟348ms,脚本加载阻塞了交互 导致用户交互卡顿
![]()
优化策略
-
拆分长任务:使用
scheduler.yield()或 scheduler.postTask 让出主线程 -
延迟非关键任务:使用
requestIdleCallback在空闲时执行 -
优化第三方脚本:异步加载、延迟执行
阶段二:事件处理(Processing Time)
定义:事件处理器从开始执行到执行完成所花费的时间。
原因:处理器内部执行耗时操作(复杂计算、大量 DOM 操作、同步网络请求、强制同步布局),阻塞主线程。
实际案例
// ❌ 问题:用户点击更换背景,但画布操作耗时,看不到即时反馈
// 来源:src/core/FTCanvasRenderer.ts - updateBackground
async updateBackground({ bgUrl, bgColor, size, callback }) {
FTBgremoveStore.changeBackgroundLoading = true; // 状态已更新,但浏览器还没渲染
// 同步执行耗时操作,阻塞主线程
const blobUrl = await FTBlobCacheManager.getInstance().addCache(bgUrl); // 100-200ms
bg.setSource(blobUrl, "imageUrl", () => {
this.blurBackground(currentPageData.blurValue || 0); // 50-100ms 模糊处理
fabricCanvas.requestRenderAll(); // 触发重绘
FTBgremoveStore.changeBackgroundLoading = false;
});
}
// ✅ 优化:先让浏览器渲染反馈,再执行耗时操作
import { nextTick } from 'src/utils';
async updateBackground({ bgUrl, bgColor, size, callback }) {
FTBgremoveStore.changeBackgroundLoading = true;
// 让出主线程,确保浏览器先渲染 loading 状态
await nextTick();
// 现在执行耗时操作
const blobUrl = await FTBlobCacheManager.getInstance().addCache(bgUrl);
bg.setSource(blobUrl, "imageUrl", async () => {
await nextTick(); // 再次让出,确保模糊处理不阻塞
this.blurBackground(currentPageData.blurValue || 0);
fabricCanvas.requestRenderAll();
FTBgremoveStore.changeBackgroundLoading = false;
});
}
优化策略
-
先反馈再处理:使用
nextTick()让浏览器先渲染状态变化 -
拆分长任务:将耗时操作拆成多个小任务
-
使用 Web Worker:将计算密集型任务移到 Worker 线程
阶段三:呈现延迟(Presentation Delay)
定义:从事件处理器执行完成,到浏览器完成下一次 paint(绘制)之间的时间。
原因:浏览器渲染管道(样式计算 → 布局 → 绘制 → 合成)耗时过长,常见问题包括强制同步布局、大量重排、复杂 CSS 选择器。
优化策略
-
避免强制同步布局:先批量读取布局属性,再批量写入 DOM
-
减少重排重绘
-
优化 CSS 选择器:避免深层嵌套,使用类选择器
INP优化核心思路
🎯 核心原则:让用户交互后的第一个 paint 尽快发生,把重的、非必须立即执行的工作延迟或拆分。
这样做能让你推迟执行的任务不算在INP时间计算内
❌ 错误示范:阻塞渲染
// 来源:src/store/FTBgremoveStore.tsx - handleLoadSimplifyCanvas
async handleLoadSimplifyCanvas(imgUrl, id, closeLoading = true) {
this.cutoutLoading = true; // 调用了,但...
// 立即执行耗时操作,阻塞主线程
const img = await loadImageByUrl(imgUrl); // 100ms 加载图片
await FTSimpleCanvasRenderInstance.uploadTop(imgUrl); // 80ms 上传到画布
await FTSimpleCanvasRenderInstance.uploadBack(page.backgroundColor); // 50ms 设置背景
this.setCurrentPage({ cutOriginUrl: imgUrl, cropImage: image, ... }); // 同步更新状态
this.cutoutLoading = false;
}
✅ 正确示范:先反馈,再干活
import { nextTick } from 'src/utils';
async handleLoadSimplifyCanvas(imgUrl, id, closeLoading = true) {
// 1. 同步更新状态
this.cutoutLoading = true;
// 2. 关键:让出主线程,给浏览器渲染机会
await nextTick(); // 浏览器有机会渲染 loading 状态
// 3. 现在才开始执行重任务
const img = await loadImageByUrl(imgUrl);
await FTSimpleCanvasRenderInstance.uploadTop(imgUrl);
await FTSimpleCanvasRenderInstance.uploadBack(page.backgroundColor);
// 再次让出,确保状态更新能及时渲染
await nextTick();
this.setCurrentPage({ cutOriginUrl: imgUrl, cropImage: image, ... });
this.cutoutLoading = false;
}
拆分和调度任务、让事件能快速响应。
![]()
工具函数:nextTick 实现
项目中已经实现了优化的 nextTick 函数(src/utils/index.ts),通过让出主线程控制权,确保浏览器有机会完成渲染和响应用户交互:
/**
* nextTick - 将任务推迟执行,优化 INP (Interaction to Next Paint)
*
* 改进点:
* 1. 优先使用 scheduler.postTask (如果可用) 来更好地控制任务优先级
* 2. 使用 MessageChannel 确保任务在下一个事件循环执行,不阻塞渲染
* 3. 双重 requestAnimationFrame 作为降级方案,确保 DOM 更新后执行
*
* @param callBack 要执行的回调函数
* @returns Promise<void>
*/
export function nextTick(callBack?: () => void) {
return new Promise<void>((resolve) => {
// 优先使用 scheduler.postTask (Chrome 94+, 更好的任务调度)
if (typeof (window as any).scheduler !== 'undefined' && (window as any).scheduler.postTask) {
(window as any).scheduler.postTask(() => {
callBack?.();
resolve();
}, { priority: 'user-blocking' });
return;
}
// 使用 MessageChannel 确保在下一个事件循环执行,不阻塞渲染
// 这对于 INP 优化很重要,因为它让浏览器有机会处理用户交互
if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
channel.port1.onmessage = () => {
callBack?.();
resolve();
};
channel.port2.postMessage(null);
return;
}
// 降级方案:双重 requestAnimationFrame
// 第一个 rAF 确保在浏览器重绘之前,第二个 rAF 确保在重绘之后
// 这样可以确保 DOM 更新已经完成
if (requestAnimationFrame) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
callBack?.();
resolve();
});
});
return;
}
// 最后的降级方案
if (setImmediate) {
setImmediate(() => {
callBack?.();
resolve();
});
} else {
setTimeout(() => {
callBack?.();
resolve();
}, 0);
}
});
}
使用示例
使用 nextTick() 主动让步。
// 使用项目中的 nextTick 函数
import { nextTick } from 'src/utils';
async function processHeavyCanvas() {
doLightWork();
// 让浏览器有机会处理待渲染的帧和用户交互
await nextTick();
// 这样doHeavyWork会被拆分成单独任务延迟执行
doHeavyWork();
}
为什么选择这些 API?
| API | 优点 | 兼容性 |
|---|---|---|
scheduler.postTask |
支持优先级控制,专为任务调度设计 | Chrome 94+ |
MessageChannel |
宏任务,确保在渲染后执行 | 广泛支持 |
双重 rAF
|
确保在下一帧渲染完成后执行 | 广泛支持 |
setTimeout(0) |
最终兜底方案 | 全平台 |
实战检测与调试
使用 Chrome DevTools 分析 INP
cpu 4 - 6倍降速,模拟低性能设备。 对比本地和线上
![]()
点点功能,观察Performace面板实时的INP
![]()
每次交互的实时INP都会记录在这里。 多点点自己的项目,顺着INP变高的那一步操作找问题即可。
使用 Web Vitals 库监控
import { onINP } from 'web-vitals';
onINP((metric) => {
console.log('INP:', metric.value);
console.log('Entries:', metric.entries);
// 上报到分析平台
if (metric.value > 200) {
analytics.track('slow_interaction', {
value: metric.value,
entries: metric.entries,
target: metric.entries[0]?.target,
});
}
});
分析线上用户真实指标
查看线上真实用户指标
Chrome 用户体验报告(crux)
总结
核心要点
-
INP 测量三个阶段:输入延迟、事件处理、呈现延迟
-
优化目标:确保 75% 的交互在 200ms 内完成
-
核心策略:先反馈,再处理;拆分长任务;让出主线程
-
工具支持:使用
nextTick、scheduler.yield()、Web Worker -
真实用户INP观察: crux查看用户真实INP变化,持续优化。
INP 优化的本质是:让用户"感觉"交互是即时响应的,哪怕后台还在默默干活。
参考资料: