
页面已经打开,接口也很快,但点击筛选、展开弹窗或输入搜索词时仍会“顿一下”。这类问题经常不在服务端,而是某段同步 JavaScript 长时间占住了浏览器主线程:输入无法处理,样式和布局不能提交,下一帧也画不出来。与其凭感觉删代码,不如先记录长任务发生的时间、持续时间和业务动作,再做有证据的优化。
先把“卡顿”说清楚
浏览器主线程一次只能执行一个任务。脚本计算、JSON 解析、创建大量 DOM、同步读取布局等操作若迟迟不返回,后面的点击、滚动和渲染只能排队。浏览器把持续时间超过 50ms 的主线程任务称为长任务;它是诊断阈值,不是用户体验的唯一标准。
| 用户现象 | 优先收集的证据 | 常见根因 |
|---|---|---|
| 点击后迟迟没有反馈 | 长任务时间点、业务标记 | 同步过滤、序列化或初始化 |
| 列表滚动掉帧 | 性能录制、布局读写顺序 | 大量 DOM、强制同步布局 |
| 首次进入页面发闷 | 长任务数量、第三方脚本 | 解析、组件挂载、SDK 抢占 |
因此,目标不是把每个任务都压到零,而是先找出会阻塞关键交互的那一段,并建立优化前后的可比较数据。
用 PerformanceObserver 留下现场

PerformanceObserver 可以在运行中订阅性能条目。先做特性检测:不同浏览器和嵌入式 WebView 的支持情况可能不同,缺少 longtask 时应安静降级,而不是让监控代码报错。
1 | function observeLongTasks(report) { |
上报时保留路由、发布版本、设备档位和持续时间即可;不要把查询参数、输入内容或完整堆栈直接送进分析系统。长任务可能连续出现,生产环境宜按页面聚合、抽样并批量发送,避免“为了观测又制造更多工作”。
给业务动作加上名字
长任务条目告诉你“何时卡”,却不一定告诉你“谁卡”。在筛选、排序、首屏挂载等关键边界补上 mark 和 measure,再把测量结果与长任务的时间窗口对齐,排查范围就会从整个页面缩到一个业务动作。
1 | function measureSync(name, work) { |
这里的名称应稳定且有限,例如 orders:filter,不要拼入用户 ID 或搜索词。发现同一测量区间经常覆盖长任务后,再用浏览器的 Performance 面板录制该交互,结合调用栈确认究竟是数据处理、框架更新还是布局抖动。监控负责发现趋势,录制负责定位代码,二者不要互相替代。
把大任务拆成可让出的片段

如果瓶颈来自可分割的纯计算,不要在一次点击中处理全部数据。下面的示例每完成一小段就把控制权交回事件循环,让浏览器有机会处理输入和绘制:
1 | function yieldToBrowser() { |
chunkSize 不是固定答案:在目标设备和真实数据量下观测,再调整到交互可接受的范围。超长列表更适合虚拟滚动;不依赖 DOM 的重计算可移到 Web Worker;布局代码则应先集中读取,再集中写入,避免反复触发同步布局。不要只把任务切碎却让总计算量继续膨胀。
| 根因类型 | 小步改法 | 验证方式 |
|---|---|---|
| 一次性处理大量数据 | 分片计算或移入 Worker | 长任务数量与时长下降 |
| 渲染全部列表项 | 虚拟列表或按需分页 | 滚动时主线程更平稳 |
| 读写布局交错 | 批量读取后统一写入 | Performance 面板不再反复布局 |
| 第三方脚本抢占 | 延后加载、隔离或移除 | 首屏关键操作更早可用 |
用同一套场景复测
优化前先固定复现步骤、数据规模和设备条件,记录每次操作的长任务数量、最长持续时间及关键测量值;优化后在同一场景重跑。只看平均值很容易掩盖偶发尖峰,至少还要看高分位和最慢样本。若数字变好但交互仍不顺畅,继续检查网络等待、动画、布局和输入延迟,而不是执着于某一个指标。
长任务不是判决书,而是一盏定位灯。把它与业务标记、性能录制和前后对比连成闭环,前端性能优化才能从“感觉好多了”变成可复查、可回归的工程结果。