理解 INP 的构成
优化 INP 之前,需要理解它衡量的是什么。一次交互的响应时间由三部分组成:
输入延迟(Input Delay):从用户触发交互到事件处理函数开始执行的时间。主要受主线程繁忙程度影响。
处理时间(Processing Time):事件处理函数的执行时间。这是开发者代码直接控制的部分。
呈现延迟(Presentation Delay):从事件处理完成到浏览器完成下一帧绘制的时间。包括样式计算、布局和绘制。
INP = 输入延迟 + 处理时间 + 呈现延迟
优化 INP 就是优化这三个环节中的一个或多个。
输入延迟优化
问题诊断
输入延迟高意味着用户点击时,主线程正在忙别的事情。常见原因:
- 页面加载时大量 JavaScript 执行
- 定时器或动画帧回调占用主线程
- 第三方脚本阻塞
解决策略
延迟非关键 JavaScript:
<!-- 非关键脚本用 defer 或 async -->
<script src="analytics.js" async></script>
<!-- 或者动态加载 -->
<script>
// 首屏渲染后再加载非关键功能
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('./non-critical-feature.js')
})
}
</script>
拆分初始化代码:
不要在页面加载时一次性执行所有初始化逻辑。按优先级分批执行:
- 首屏渲染必需的代码立即执行
- 可见区域的交互在 requestIdleCallback 中初始化
- 折叠区域的功能在用户滚动到附近时懒加载
控制第三方脚本:
第三方脚本是输入延迟的常见罪魁祸首。策略:
- 审计所有第三方脚本,移除不必要的
- 使用 Partytown 将第三方脚本移到 Web Worker
- 延迟加载不影响首屏的第三方脚本
处理时间优化
问题诊断
事件处理函数执行时间长,可能因为:
- 大量 DOM 操作
- 复杂的状态计算
- 同步的数据处理
- 阻塞式 API 调用
解决策略
减少 DOM 操作:
DOM 操作是出了名的慢。优化方式:
- 批量更新而非逐个更新
- 使用 DocumentFragment 批量插入
- 用 CSS 类切换代替直接样式修改
- 虚拟列表处理大量数据
避免强制同步布局:
读取某些属性会触发浏览器强制计算布局:
// 差的写法:读写交替,每次读取都触发布局
for (const item of items) {
item.style.width = item.offsetWidth + 10 + 'px'
}
// 好的写法:先批量读取,再批量写入
const widths = items.map(item => item.offsetWidth)
items.forEach((item, i) => {
item.style.width = widths[i] + 10 + 'px'
})
拆分长任务:
单个任务执行超过 50ms 就被认为是"长任务",会阻塞交互响应。拆分方式:
// 差的写法:一个循环跑完
function processData(items) {
for (const item of items) {
heavyComputation(item)
}
}
// 好的写法:分批处理,让出主线程
async function processData(items) {
const BATCH_SIZE = 100
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE)
batch.forEach(heavyComputation)
// 让出主线程
await new Promise(resolve => setTimeout(resolve, 0))
}
}
使用 Web Worker:
真正的 CPU 密集型任务应该移到 Worker:
// 主线程
const worker = new Worker('processor.js')
worker.postMessage(data)
worker.onmessage = (e) => {
updateUI(e.data)
}
// processor.js (Worker)
self.onmessage = (e) => {
const result = heavyComputation(e.data)
self.postMessage(result)
}
呈现延迟优化
问题诊断
事件处理完成后,浏览器需要计算样式、布局、绘制。如果这些步骤耗时长,INP 会受影响。
常见原因:
- 复杂的 CSS 选择器
- 大面积的重绘
- 频繁触发布局
- 大量 DOM 元素
解决策略
简化 CSS 选择器:
/* 差的写法:复杂选择器 */
.container > div:nth-child(odd) .item:not(.disabled) span {}
/* 好的写法:直接类名 */
.item-text {}
使用 CSS containment:
告诉浏览器某个元素的布局不影响外部:
.card {
contain: layout style;
}
/* 更激进的隔离 */
.independent-widget {
contain: strict;
}
优化动画性能:
- 只动画 transform 和 opacity(不触发布局)
- 使用 will-change 提示浏览器
- 避免动画过程中触发布局
.animated-element {
will-change: transform;
}
/* 动画只用 transform */
.slide-in {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
减少 DOM 大小:
DOM 元素越多,布局计算越慢。策略:
- 虚拟滚动展示长列表
- 延迟渲染不可见内容
- 清理不再需要的 DOM 元素
框架特定优化
React 优化
使用 Concurrent 特性:
import { useTransition, useDeferredValue } from 'react'
function SearchResults({ query }) {
const [isPending, startTransition] = useTransition()
const deferredQuery = useDeferredValue(query)
function handleSearch(e) {
// 标记为低优先级更新,不阻塞交互
startTransition(() => {
setSearchQuery(e.target.value)
})
}
return (
<>
<input onChange={handleSearch} />
{isPending && <Spinner />}
<ResultsList query={deferredQuery} />
</>
)
}
避免不必要的重渲染:
// 使用 memo 避免重渲染
const ExpensiveList = memo(function ExpensiveList({ items }) {
return items.map(item => <Item key={item.id} data={item} />)
})
// 使用 useMemo 缓存计算结果
function Dashboard({ data }) {
const processedData = useMemo(
() => expensiveProcess(data),
[data]
)
return <Chart data={processedData} />
}
Vue 优化
使用 v-memo:
<template>
<!-- 只有 item.id 或 selected 变化时才更新 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, selected === item.id]">
{{ item.name }}
</div>
</template>
计算属性 vs 方法:
<script setup>
// 好的写法:计算属性有缓存
const filteredList = computed(() => {
return list.filter(item => item.active)
})
// 差的写法:每次渲染都执行
function getFilteredList() {
return list.filter(item => item.active)
}
</script>
测量与验证
开发阶段测量
Chrome DevTools 的 Performance 面板可以详细分析交互:
- 开始录制
- 执行要测试的交互
- 停止录制
- 查看 "Interactions" 轨道
注意 "Long Task" 标记,它们是 INP 问题的信号。
生产环境监控
import { onINP } from 'web-vitals'
onINP((metric) => {
// metric.value: INP 值(毫秒)
// metric.entries: 相关的 PerformanceEntry
// metric.attribution: 归因信息
console.log({
value: metric.value,
element: metric.attribution?.eventTarget,
eventType: metric.attribution?.eventType
})
// 上报到监控平台
sendToAnalytics({
name: 'INP',
value: metric.value,
page: location.pathname,
...metric.attribution
})
})
设置性能预算
// 性能预算检查
const INP_BUDGET = 200 // 毫秒
onINP((metric) => {
if (metric.value > INP_BUDGET) {
console.warn(`INP 超出预算: ${metric.value}ms > ${INP_BUDGET}ms`)
// 触发告警
}
})
优先级排序
不是所有交互都同样重要。优化策略:
高优先级:
- 主要的用户操作路径(添加购物车、提交表单)
- 导航和页面切换
- 核心功能的触发按钮
中优先级:
- 筛选和排序
- 展开/折叠
- 非关键的状态切换
低优先级:
- 辅助功能(分享、收藏)
- 不影响核心流程的交互
先优化高优先级交互,确保核心体验流畅。
常见场景解决方案
无限滚动列表
问题:滚动时不断加载新内容,DOM 越来越大。
方案:
- 虚拟滚动,只渲染可见区域
- 内容分页,限制单页数量
- 回收离开视口的 DOM 元素
复杂表单
问题:大量表单字段,每次输入都触发验证。
方案:
- 防抖验证输入
- 延迟验证非关键字段
- 使用非受控组件减少重渲染
数据密集型仪表盘
问题:大量图表和数据表格,交互时卡顿。
方案:
- 图表使用 Canvas/WebGL 而非 SVG
- 数据聚合在服务端完成
- 按需加载图表,不在首屏全部渲染
总结
INP 优化的核心策略:
| 环节 | 优化方向 |
|---|---|
| 输入延迟 | 减少主线程阻塞 |
| 处理时间 | 优化事件处理代码 |
| 呈现延迟 | 简化渲染计算 |
关键行动:
- 建立 INP 监控,获取基线数据
- 识别 INP 最差的交互
- 分析瓶颈在哪个环节
- 针对性优化
- 验证改进效果
INP 优化不是一劳永逸的,随着功能迭代需要持续关注。把 INP 监控纳入 CI/CD 流程,防止性能回退。
相关文章推荐:


