配了五年 SPA,今天才发现 Chrome 根本不知道你换过路由——直到 Chrome 151

你做了一个 SPA,用户从首页点进详情页,URL 变了,页面内容变了,用户的感受是「翻页了」。但 Chrome 不知道。

这不是 Chrome 的 bug,是设计问题。


问题:Core Web Vitals 是给传统网页设计的

Core Web Vitals 的三个指标——LCP、INP、CLS——是围绕「文档加载」模型设计的:用户点一个链接,浏览器卸载旧页面加载新页面,计时器归零,一切重新开始。

SPA 故意打破了这个模型。路由切换发生在 JavaScript 里,document 从未卸载,浏览器不知道用户刚从一个页面去了另一个页面。

这造成了三个具体的问题:

LCP 只报一次。 首次加载时 LCP 会触发一次,然后就停了。之后的每一个路由切换,页面加载体验完全无测量数据。

INP 跨整个会话累积。 INP 报告的是整个会话期间最差的一次交互。用户在某个角落页面的一次卡顿,会污染整站 INP 数据,让它看起来比实际差很多。

CLS 无上限累积。 页面活了四十分钟,CLS 值一直在累加,哪怕中间没有任何布局偏移,也越来越难看。

团队不是没试过解决。多数团队选择自己在路由层埋点:路由切换时手动打 mark,自己定义「页面 ready」的时机,自己匹配时间戳做归因。这套方案能用,但不可跨框架比较,换个路由库就可能失效,每个团队实现还都不一样。


Chrome 151 的解法:两个新的 PerformanceEntry

Chrome 151(2026 年 7 月底稳定版)推出了两个新的性能 API,不需要任何 flag,直接在 Chrome 151+ 上可用。

1. soft-navigation:告诉浏览器「发生了路由切换」

每次 Chrome 检测到一次软导航,就会发出一个 soft-navigation 条目,其中包含:navigationId(唯一标识符)、name(新 URL)、interactionId(触发导航的交互 ID)。这个条目为后续所有性能数据建立新的时间基准,让指标可以正确归因到对应路由。

2. interaction-contentful-paint:路由的 LCP 等效指标

这个指标测量用户交互后 DOM 区域内的内容绘制。它给出的是:交互触发了路由,页面渲染完成,这中间花了多长时间。这才是 SPA 里「路由加载体验」真正的 LCP 等效指标。

早期版本把检测和 LCP 重置直接耦合——检测到软导航就自动报新的 LCP。开发者反馈这个设计有问题:检测逻辑和指标语义不应该绑在一起,否则检测算法一改,LCP 的定义就变了。Chrome 151 最终把两者解耦:检测归检测,ICFP 归 ICFP,两个独立工作,互不影响。


Chrome 是怎么「猜」出来发生了路由的

不是精确的 API,是启发式(heuristic),需要三个条件同时满足:

  1. 交互是用户触发的(不是定时器、不是后台轮询)
  2. URL 发生可见变化,并且创建了新的历史记录条目(pushState/replaceState 里,只有 pushState 算数)
  3. 交互导致了可见绘制

反向排除也很明确:纯 JS 更新没有用户交互,不算;replaceState 用于翻页/筛选/滚动恢复,不算,因为它没有创建新的历史条目。

这是一个设计决策:不做开发者定义的标记接口,Chrome 用统一的启发式规则来处理,这样不管你用 React Router、Vue Router 还是 SvelteKit,行为一致,不需要每个框架单独适配。


怎么用:两行代码接入

可以用 PerformanceObserver 监听,Feature Detection 写法:

if (PerformanceObserver.supportedEntryTypes.includes(soft-navigation)) {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.entryType === soft-navigation) {
        console.log(路由切换:, entry.name, entry.navigationId);
      }
      if (entry.entryType === interaction-contentful-paint) {
        console.log(路由渲染完成:, entry.duration);
      }
    }
  });
  observer.observe({ types: [soft-navigation, interaction-contentful-paint], buffered: true });
}

更简单的方式是用 web-vitals 库,从 v6.0.0 开始支持,提供了 reportSoftNavs 选项:

import { onLCP, onINP, onCLS } from web-vitals;

function sendToAnalytics(metric) {
  // metric 中现在包含 navigationId 和 navigationURL
  // 可以正确归因到对应路由
  fetch(/analytics, { body: JSON.stringify(metric) });
}

// 传统硬导航数据
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

// 软导航数据(Chrome 151+)
onLCP(sendToAnalytics, { reportSoftNavs: true });
onINP(sendToAnalytics, { reportSoftNavs: true });
onCLS(sendToAnalytics, { reportSoftNavs: true });

navigationId 和 navigationURL 会随指标一起传递,不需要再靠时间戳匹配路由。


一个重要前提:不影响排名

Google 明确表示:目前没有计划把软导航数据纳入 Chrome User Experience Report(CrUX),暂时没有 SEO/排名影响。

这意味着这个 API 是给 RUM 用的,不是给搜索爬虫用的。你在 CrUX/PageSpeed Insights 里看到的还是首屏硬导航数据。

但对 RUM 的影响是真实的:Datadog、SspeedCurve、Web Vitals.js 用户现在可以第一次看到每个路由的 LCP、INP、CLS 了。这让 SPA 的性能工作从「凭经验感觉」变成「有数据可循」。


下一步可以做什么

第一件事:检查你用的 web-vitals 版本,如果是 6.0.0 以下,先升级。然后加一行 { reportSoftNavs: true },这一步就能让你看到 SPA 各路由的真实性能数据。

拿到数据之后再谈优化。优化什么、优先优化哪个路由,靠经验猜不如用这个数据来排。

评论区

0 条评论

登录后可评论。

阿速·性能优化 12 阅读