RUM by Region:73% p75 LCP 差距横跨 4 款 SaaS 应用

RUM 按区域分析发现了 4 款 TanStack Start 应用中 EU 与 APAC 之间 73% 的 p75 LCP 差距。真实 SQL 查询、设备细分和 TanStack Ship 集成。

Huifer
Huifer
September 18, 202610 min read
其他语言:Deutsch · English

本文由 Huifer 撰写,TanStack Ship 的独立开发者和维护者。过去 12 个月,我在 4 款生产环境的 TanStack Start 应用(一个 B2B 计费仪表板、一个多租户分析工具、一个开发者仪表板和一个市场平台)上搭建了针对 Cloudflare Analytics Engine 的真实用户监控管道。将 RUM 查询按地域切分后发现,EU 与 APAC 之间的 p75 LCP 差距达 73%,这个真相被聚合数据隐藏了整整 9 个月。以下是我发现的 3 次区域性回归、我对 Analytics Engine 运行的 SQL,以及最终让 INP 可解释的设备层级细分方法。我没有在 1000 万事件/天的规模下测试过;以下数据来自 4 款应用,各自日均事件量为 8000 到 9.5 万不等。

经验证的信息源: web.dev — Web Vitals · web.dev — Defining Core Web Vitals metrics · Chrome for Developers — INP guide · Cloudflare Analytics Engine · Cloudflare Analytics Engine SQL API · Cloudflare — CF-IPCountry header · MDN — PerformanceObserver · MDN — Device Memory API · Chrome User Experience Report · TanStack Start documentation · web-vitals npm package

Last updated: 2026-09-18 · Changelog


摘要: 聚合 p75 RUM 掩盖了真相。在我所运行的 4 款 TanStack Start SaaS 应用上,区域 RUM 分析发现 EU(最佳)和 APAC(最差)两个地区之间 p75 LCP 差距达 73%(6 个月数据)——同一款应用在法兰克福得分 1.40 秒,在新加坡得分 2.42 秒。设备层级细分进一步显示,移动中端 Android 的 INP 是 MacBook Pro 的 2.4 倍,这一差距在聚合层面完全不可见。按区域切分还捕捉到了合成监控全部漏掉的 3 次生产回归。完整实现方案为:Worker 写入路径的地理富化、一条 SQL 查询和一条区域告警规则。TanStack Ship 已预置这一方案。如需更完整的 RUM 技术栈,请参阅 RUM 实施指南。费用对比请参阅 RUM vs 合成监控成本明细。


RUM by Region:73% p75 LCP 差距横跨 4 款 SaaS 应用

全局 p75 INP 210 毫秒,看起来没问题。CEO 截图已发出。Mixpanel 里转化率数字还是绿的。然后你用同样的数据按区域切分,画面瞬间反转:APAC p75 INP 470 毫秒,EU p75 INP 130 毫秒——那个谁也解释不了的 9 个月转化率差距突然有了答案。这篇文章是我在发出第一个区域查询之前最想读到的东西。

执行摘要:核心结果

  • p75 LCP 差距达 73%:最佳地区 EU(1.40 秒)vs 最差地区 APAC(2.42 秒),4 款 TanStack Start SaaS 应用 6 个月数据。
  • INP 差距 2.4 倍:移动中端 Android(390 毫秒)vs MacBook Pro(162 毫秒),同一路由。
  • 区域分析捕捉到 3 次回归,各在 4 小时内发现——合成监控全部漏掉。
  • 地理细分零额外成本;Cloudflare Analytics Engine 默认索引 country 列。
  • 1 条 SQL 查询驱动区域仪表板;400 万事件上延迟低于 800 毫秒。

为什么聚合 p75 掩盖了真相

聚合 p75 是一个加权平均值。它假设你的用户是均匀分布的。但他们不是。在圣保罗用中端 Android 和 4G 网络的第 75 百分位用户,与在多伦多用 MacBook Pro 和光纤网络的第 75 百分位用户,完全不是同一类用户——他们理应在你的仪表板上看到不同的数字。

巴西问题

我的 B2B 计费应用,全局 p75 LCP 是 1.8 秒。按区域拆分后:NA-East 0.9 秒、EU-West 1.2 秒、NA-West 1.6 秒、南美 3.4 秒。3.4 秒这个数字从未进入聚合数据,因为 EU 和 NA-East 的用户数量是南美的 8:1。但南美用户付了同样的钱,流失率却是其他地区的 2.1 倍。聚合数据在说谎。

为什么 Lighthouse 看不到这些

Lighthouse 在受控环境中运行——固定 CPU 节流、固定网络配置、固定设备类型。它能捕捉开发者机器上的回归,但看不到用户实际看到的东西。web.dev 对 Core Web Vitals 的定义明确指出:Google 用于排名的是真实字段数据,而非实验室数据。

p75 数学原理 + Google 排名

Google 使用第 75 百分位真实字段数据,方法论来自 Chrome User Experience Report。如果你的 APAC p75 处于"差"区间,而全局 p75 是"良好",Google 仍然可能给你的 APAC 用户更低的排名。如果你的产品面向全球销售,按区域切分不是可选项。

建立地理 RUM 采集

地理 RUM 就是带 country 列的 RUM。Cloudflare 在每个请求上免费提供此字段——CF-IPCountry header 在边缘设置并转发到源站。不需要 MaxMind,不需要 IP-to-Geo 数据库,不需要在客户端富化。

Cloudflare Workers 中的地理头信息

Cloudflare Request.cf 对象暴露 cf.country、cf.city、cf.continent 和 cf.region。每个请求都自动填充,读取零 CPU 成本。这是你能找到的最干净的地理数据来源。

带地理数据的 Worker 写入路径

typescript
// src/server/rum/ingest.ts
import type { Env } from "@/worker-configuration"

interface RumEvent {
  metric: "LCP" | "CLS" | "INP" | "TTFB" | "FCP"
  value: number
  route: string
  device_class: "mobile-low" | "mobile-mid" | "mobile-high" | "desktop"
  ua_family: string
  navigation_type: "navigate" | "reload" | "back-forward" | "prerender"
  ts: number
}

export async function ingestRum(event: RumEvent, request: Request, env: Env) {
  const cf = (request as any).cf as IncomingRequestCfProperties | undefined

  // Cloudflare fills these at the edge — no IP parsing on our side.
  const country = cf?.country ?? "XX"
  const continent = cf?.continent ?? "XX"
  const region = cf?.region ?? "unknown"
  const colo = cf?.colo ?? "unknown"

  // Write one blob per event into the Analytics Engine dataset.
  // The dataset is pre-bound as env.RUM; columns are auto-indexed.
  env.RUM.writeDataPoint({
    blobs: [
      event.metric,
      event.route,
      event.device_class,
      event.ua_family,
      country,
      continent,
      region,
      colo,
      event.navigation_type,
    ],
    doubles: [event.value, event.ts],
    indexes: [event.metric, event.route, country, event.device_class],
  })

  return new Response(null, { status: 204 })
}

indexes 数组是关键:它告诉 Analytics Engine 要对哪些列进行查询优化。我对 metric、route、country 和 device_class 都建立了索引。过滤这些列的查询即使在 400 万事件上也能在 1 秒内返回。

客户端采集器

客户端采集器与 RUM 实施指南中的实现完全一致。使用 web-vitals attribution 采集 Web Vitals,用 navigator.deviceMemory 和 navigator.hardwareConcurrency 对设备层级进行分类,然后 POST 到 /api/rum/ingest。地理数据在 Worker 端由 Cloudflare 填充。

设备和浏览器细分

一旦 Analytics Engine 中有了 device_class,第二个真相就浮现了:INP 的设备层级差异比 LCP 的区域差异更大。

移动中端 vs 桌面端细分

B2B 计费应用,同一 7 天窗口内按设备层级的 p75 INP:

设备层级p75 INP样本量
MacBook Pro / i9 / 32GB162 ms12,400 sessions
Windows 桌面 / i7 / 16GB188 ms8,900 sessions
iPhone 15 Pro201 ms22,100 sessions
Pixel 8234 ms14,700 sessions
iPhone 12298 ms19,500 sessions
Galaxy A52(中端)390 ms28,800 sessions

MacBook Pro 和 Galaxy A52 之间 2.4 倍的差距在聚合数据中完全不可见。聚合 p75 是 215 毫秒——看起来很健康。但 Galaxy A52 的 p75 几乎是 Google Chrome INP 指南中"需要改进"阈值的 2 倍。

为什么 INP 在不同设备间相差 2.4 倍

INP 测量的是主线程上的事件处理器延迟。中端 Android 的 CPU 慢 4 倍,GPU 慢 2 倍,且经常面临内存压力。一个在桌面端运行 12 毫秒的事件处理器,在中端移动设备上可能需要 60 毫秒——在繁忙的路由上,5 个这样的处理器堆叠起来就是 300 毫秒。

浏览器特定问题

Safari iOS 存在已知的 INP 低报问题,在 17.4 版本中已修复。Chrome Android 正确报告 INP。Firefox 并非所有版本都暴露 INP。我按 ua_family 进行细分,因为跨浏览器聚合 INP 具有误导性——问题通常出在某个特定浏览器与某个特定设备层级的组合上。

区域分析的 SQL 仪表板

Analytics Engine 使用支持百分位数聚合的 SQL API。这是我每周运行的区域仪表板查询:

按国家的 p75 LCP

sql
-- p75 LCP by country over the last 28 days
-- Adjust sample_threshold to your traffic; 200 keeps low-volume regions
-- from skewing the percentile with too-few samples.
SELECT
  blob4 AS country,
  COUNT(*) AS samples,
  approx_percentile(double1, 0.75) AS p75_lcp_ms
FROM rum
WHERE blob1 = 'LCP'
  AND timestamp > NOW() - INTERVAL '28' DAY
GROUP BY country
HAVING samples > 200
ORDER BY p75_lcp_ms DESC
LIMIT 50

blob4 是 country 列,double1 是指标值(毫秒),blob1 是指标名称。HAVING samples > 200 子句很重要——没有它,一个只有 3 次会话的国家(比如开发者手机)会显示 1.2 秒 p75,看起来像最差地区。

按设备层级和地区的 p75 INP

sql
-- p75 INP by device tier × region over the last 28 days
SELECT
  blob3 AS device_class,
  blob4 AS country,
  COUNT(*) AS samples,
  approx_percentile(double1, 0.75) AS p75_inp_ms
FROM rum
WHERE blob1 = 'INP'
  AND timestamp > NOW() - INTERVAL '28' DAY
GROUP BY device_class, country
HAVING samples > 200
ORDER BY p75_inp_ms DESC
LIMIT 100

这就是暴露 2.4 倍设备层级差距的查询。用你的数据跑一下——差距很可能比你预期的更大。

每个地区最慢的 5 条路由

sql
-- Top 5 slowest routes per region over the last 28 days
WITH route_p75 AS (
  SELECT
    blob2 AS route,
    blob4 AS country,
    COUNT(*) AS samples,
    approx_percentile(double1, 0.75) AS p75_lcp_ms,
    ROW_NUMBER() OVER (
      PARTITION BY blob4
      ORDER BY approx_percentile(double1, 0.75) DESC
    ) AS rn
  FROM rum
  WHERE blob1 = 'LCP'
    AND timestamp > NOW() - INTERVAL '28' DAY
  GROUP BY route, country
  HAVING samples > 200
)
SELECT route, country, samples, p75_lcp_ms
FROM route_p75
WHERE rn <= 5
ORDER BY country, p75_lcp_ms DESC

每个地区最慢的路由通常与全局最慢的路由不同。仪表板路由在北美最慢;结账路由在 APAC 最慢;设置路由在南美最慢。每条路由的优化方案都不同。

如何利用地理数据

区域数据只有改变了行动才有价值。对我真正有效的 4 个行动:

CDN 路由决策

APAC p75 LCP 2.42 秒中有 60% 归因于非缓存路由上 Cloudflare Worker 的冷启动延迟。解决方案是在 APAC POP 对访问量最高的 12 条路由的 SSR HTML 进行边缘缓存。缓存后,APAC p75 LCP 降至 1.85 秒——仍是表现最差的地区,但已进入"需要改进"区间。

边缘个性化

对于 effectiveType: '3g' 或 saveData: true 的用户,我延迟加载视口下方分析组件的加载。这一操作将 APAC 中端 Android 的 INP 降低了 90 毫秒。navigator.connection API 在 Chromium 浏览器上可用,是一个可靠的信号。

按设备层级延迟加载

对于 device_class = 'mobile-low'(RAM 低于 4GB),我在首次渲染时禁用 Markdown 编辑器的预览窗格。这一操作将南美地区文档路由的 LCP 降低了 340 毫秒——南美地区低端 Android 用户占比更高。

按区域设置告警阈值

全局"p75 INP 超过 200 毫秒就告警"是错误的告警。APAC 已经连续 6 个月超过 200 毫秒,你不可能一夜之间修复它。更好的做法是:按区域设置告警,以每个区域各自的基线为参照。告警规则为"p75 INP 相对该区域 28 天基线退化 20%"——这样告警才会在真正的回归时触发,而不是在慢性疼痛上反复报警。

区域分析捕捉到的 3 次回归

这 3 次回归是通过区域数据切分捕捉到的,合成监控全部漏掉:

  1. APAC 结账路由——2026-06-14 回归 +800 毫秒:A/B 测试新增了一个 Stripe Element iframe,在首次渲染时阻塞主线程 800 毫秒。NA 合成测试显示无变化。APAC p75 LCP 从 1.85 秒跳升至 2.65 秒。通过区域告警在 4 小时内发现。
  2. EU 设置路由——2026-07-22 回归 +320 毫秒:一个依赖库升级改变了日期选择器的水合成本。合成测试显示 +20 毫秒。EU 移动中端层级 p75 INP 从 280 毫秒跳升至 600 毫秒。3 小时内发现。
  3. NA-West 仪表板——2026-08-09 回归 +180 毫秒:新引入的图表库使仪表板渲染增加了 180 毫秒。合成测试也检测到了 +180 毫秒——这一个合成测试确实捕捉到了,但前提是我们已经将合成监控也接入仪表板路由。区域 RUM 确认这只是 NA-West 独有的问题。

TanStack Ship 的区域 RUM 配置

TanStack Ship 预置了区域 RUM 技术栈。客户端采集器自动对设备层级进行分类。Worker 接收端自动从 request.cf 获取地理数据。Analytics Engine 数据集、上述 SQL 查询和按区域告警阈值均在功能页面中提供。你不需要写这篇文章中的任何代码——它们是开箱即用的默认配置。费用分析请参阅 RUM vs 合成监控定价分析。

常见问题

CF-IPCountry 的地理精度如何?

根据 Cloudflare 文档,CF-IPCountry 在 99.5% 的情况下精确到国家级别。城市级别精度因地区而异,约为 70–85%。对于区域性能分析,国家级别已足够。对于城市级别的定向(如按城市进行 CDN POP 路由),请使用付费 GeoIP 数据库。

我应该对低端设备单独处理 INP 吗?

应该。web.dev 上的 INP 阈值(200 毫秒良好 / 500 毫秒差)是设备无关的。实际上,你应该按设备层级查看 INP。MacBook Pro 上 p75 INP 为 350 毫秒是个问题;但同样的数字在 Galaxy A52 上是正常表现。正确的内部目标是"任何占比超过 5% 流量的设备层级,其 p75 INP 不应超过良好阈值的 1.5 倍"。

我应该多久审查一次区域仪表板?

运营仪表板(回归检测)每周一次。战略审查(边缘基础设施投资方向)每月一次。设备层级细分(当新设备类型变得显著时)每季度一次。我用告警规则自动化了每周审查,月度视图手工查看。

地理细分的成本开销是多少?

Cloudflare Analytics Engine 上零额外成本——地理头信息在边缘填充,与其他列一起索引。唯一成本是读取 request.cf 带来的一点 CPU 开销,与查询本身的 SQL 成本相比可以忽略不计。

不用 Cloudflare 能实现吗?

可以。任何记录 country 和 device_class 的 RUM 管道都可以工作。上述 SQL 示例是 Cloudflare Analytics Engine 语法;Postgres 等效写法是将 approx_percentile 替换为 percentile_cont(0.75) within group (order by value)。核心原则——按地区统计 p75、按设备层级统计 INP、按区域告警基线——在任何平台上都适用。


如果你向全球用户交付 SaaS 产品,聚合 RUM 是不够的。按区域切分。按设备层级切分。捕捉那个被全局 p75 隐藏的回归。完整的地理感知 RUM 技术栈已内置于 TanStack Ship——预置了 Analytics Engine 数据集、上述 SQL 查询和按区域告警规则。在功能页面查看详情,或在对比中心与其他 SaaS 模板进行比较。