普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月23日首页

前端性能指标速查手册

作者 antkang
2026年4月23日 17:19

前端性能指标速查手册

一、一张时间线先理清所有指标的位置

用户点链接
    │
    ▼                                                   用户能交互
    ─────────────────────────────────────────────────────┐
    0              服务器返回  DOM解析完  最大内容出现     所有资源下完
    │                 │          │           │            │
    │ ←── TTFB ──→    │          │           │            │
    │                 │ ←─ DCL ─→│           │            │
    │        ←──── FP / FCP ────→│           │            │
    │        ←──────── LCP ─────────────────→│            │
    │        ←───────────────── Load ──────────────────→  │
    │        ←───────────────── TTI ──────────────────→   │
    ▼
导航开始(performance.timeOrigin 零点)

理解所有指标的第一件事:它们都用同一把尺子量(距离导航开始多少 ms),所以能直接比较、相减。


二、核心指标,按"谁先谁后"排列

1. TTFB · Time To First Byte

"服务器返回 HTML 第一个字节要多久"

  • 回答:网络 + 服务端 有没有问题
  • 包括:DNS 解析、TCP 握手、TLS 协商、服务端处理、HTML 开始返回
  • 看它发现什么:后端慢、CDN 配错、DNS 查询慢、HTTPS 握手慢

测量方式

const nav = performance.getEntriesByType('navigation')[0];
const ttfb = nav.responseStart; // 从导航开始到收到第一个字节

达标线:< 200ms 好 / 200~600ms 可接受 / > 600ms 要改


2. FP · First Paint

"浏览器第一次往屏幕刷像素"(哪怕只是背景色)

  • 实用意义不大,因为画一个灰色背景也算 FP
  • 通常你会忽略它,直接看 FCP

3. FCP · First Contentful Paint(关键

"浏览器第一次画出有意义的内容"(文本、图片、SVG、canvas 等)

  • 用户视角:"白屏结束了"
  • 被 Google 官方定义为 Core Web Vital 的"加载"维度之一
  • 注意:SPA 里 FCP 可能是 index.html 的 loading 骨架,而不是应用真正渲染

测量方式

new PerformanceObserver((list) => {
  list.getEntries().forEach((e) => {
    if (e.name === 'first-contentful-paint') {
      console.log('FCP:', e.startTime);
    }
  });
}).observe({ type: 'paint', buffered: true });

达标线:< 1.8s 好 / 1.8~3s 可接受 / > 3s 差


4. DCL · DOMContentLoaded

"HTML 全部解析完 + 所有 defer / module script 执行完"

  • 不等图片、样式、iframe
  • 对 SPA 意义比传统页面小(SPA 的 DOM 大部分是 JS 构造的,不在 HTML 里)

测量方式

nav.domContentLoadedEventEnd;

5. LCP · Largest Contentful Paint(Core Web Vital

"页面主要视觉区域画完的时刻"(页面里最大的那个内容块)

  • 回答:用户觉得页面"差不多好了" 是什么时候
  • Google 三大 Core Web Vitals 之一,直接影响 SEO
  • 会随内容变大而不断更新,直到用户首次交互才定格
  • 比 FCP 更贴近真实用户体感

测量方式

new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lcp = entries[entries.length - 1]; // 最后一次上报的才是最终 LCP
  console.log('LCP:', lcp.startTime, 'element:', lcp.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });

达标线< 2.5s 好 / 2.5~4s 可接受 / > 4s 差(Google 硬标准)


6. Load

"HTML 声明的所有资源都下完"(含图、CSS、子 iframe)

  • 不等:动态 import、运行时 fetch、new Image
  • 在 SPA 里意义很小,常早于应用真正可用
  • 看它的主要用途:检查初始 bundle + 关键资源加载是否异常
nav.loadEventEnd;

7. TTI · Time To Interactive

"主线程连续空闲 5 秒,可稳定响应交互"

  • 传统但难精确测量(需要看 long task、FCP、资源加载多重信号)
  • 浏览器不直接暴露 —— 需要算法推断
  • 建议用 web-vitals 库或 Lighthouse 测,不要手写

8. INP · Interaction to Next Paint(Core Web Vital,2024 替换 FID

"用户每次交互(点击/输入)到下一次画面更新的耗时"

  • 可交互性(FCP/LCP 测"能看",INP 测"能用")
  • 贯穿整个页面生命周期,不是首屏指标
  • 反映 JS 主线程是否卡

测量方式:强烈建议用 web-vitals 库,手写很复杂

达标线:< 200ms 好 / 200~500ms 可接受 / > 500ms 差


9. CLS · Cumulative Layout Shift(Core Web Vital

"页面上所有意外的布局偏移累积分数"

  • 图片没写 width/height、字体替换、广告插入都会引发 CLS
  • 不是时间指标,是一个 相对分数
  • 反映视觉稳定性

达标线:< 0.1 好 / 0.1~0.25 可接受 / > 0.25 差


10. TBT · Total Blocking Time

"FCP 到 TTI 之间,主线程被长任务阻塞的总时长"

  • 长任务 = 单次执行 > 50ms 的 JS 任务
  • 反映加载阶段的 JS 臃肿程度
  • Lighthouse 核心指标,但不是 Core Web Vital

三、如何测 · 四种通用方案

方案 适用 优点 缺点
Chrome DevTools Performance 面板 一次性调试 零代码、直观、火焰图详细 手动录制,没法长期监控
Lighthouse 一次性评分 综合报告,含建议 实验室环境,不代表真实用户
自己 PerformanceObserver 长期埋点 灵活、零依赖 要写代码,边界 case 要自己处理
web-vitals 库(Google 官方) 线上监控 规范、跨浏览器、支持 INP/CLS +3KB(但值)

推荐的最小 web-vitals 接入

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

const send = (metric) => {
  // 上报到你的监控后端
  navigator.sendBeacon('/api/metrics', JSON.stringify(metric));
};

onCLS(send);
onFCP(send);
onLCP(send);
onINP(send);
onTTFB(send);

三行代码覆盖所有 Core Web Vitals,Google 官方维护,永远比自写的准。


四、自定义测量:performance.mark / performance.measure

浏览器自动提供的指标只是"公共节点"。你自己关心的节点要自己打标

// 标一个时间点
performance.mark('tab-switched');

// 标一段耗时
performance.mark('data-fetch-start');
await fetchData();
performance.mark('data-fetch-end');
performance.measure('data-fetch-duration', 'data-fetch-start', 'data-fetch-end');

// 读出来
const m = performance.getEntriesByName('data-fetch-duration')[0];
console.log(m.duration);

SPA 里最常见的自定义节点

  • app-script-start:JS 开始执行
  • app-mounted:框架首屏挂载完
  • route-change-start / route-change-end:路由跳转
  • api-call-start / api-call-end:接口调用

五、指标挑选原则(SPA 视角)

你想知道 看这个
服务端/网络慢不慢 TTFB
用户白屏多久 FCP
用户真觉得"能看了"多久 LCP
首屏真正可交互多久 自定义 app-mounted mark
每次点击是否卡顿 INP
页面是否在跳动 CLS
路由跳转多久 自定义 route-change measure
接口慢不慢 Resource Timing APIgetEntriesByType('resource')

六、一句话记住每个指标的"灵魂"

  • TTFB:网络+后端的成绩单
  • FCP:白屏结束的那一刻
  • LCP:用户觉得"页面好了"的那一刻(Google 认可的"加载"
  • DCL:HTML 骨架完成的那一刻
  • Load:HTML 声明的资源全下完(SPA 里不重要)
  • TTI:主线程稳定空闲(理论可交互)
  • INP:每次交互的响应速度(Google 认可的"交互"
  • CLS:页面跳不跳(Google 认可的"稳定"
  • TBT:加载阶段 JS 卡不卡

Core Web Vitals 三件套 = LCP + INP + CLS。这三个是 Google 搜索排名会用的,也是线上监控的最小必要集。其他都是辅助。


七、一条黄金路径建议

不上线:用 Chrome DevTools Performance + Lighthouse 足够,不用写埋点。

上线后:接 web-vitals + 自建或对接 APM(Sentry / 阿里 ARMS / 自建后端)。监控 Core Web Vitals + 若干自定义 mark 就够。

不要做的事

  • 手写 TTI / CLS / INP 计算(必错)
  • 在 dev 模式看性能下结论(几乎永远比 prod 慢 2~5 倍)
  • 只看平均值(看 p75 或 p95 才贴近真实用户体验)

昨天以前首页

Vite 开发代理里的 `ws` 是什么,什么时候该开

作者 antkang
2026年4月20日 13:19

Vite 开发代理里的 ws 是什么,什么时候该开

什么是 ws

ws 指的是 WebSocket

它和普通 HTTP 请求不一样,不是一次请求一次响应,而是浏览器和服务端建立一条持续连接,后续双方都可以持续收发消息。

常见用途:

  • 聊天
  • 实时通知
  • 推送消息
  • 开发环境里的热更新(HMR)

proxy 里的 ws: true 是什么意思

在 Vite server.proxy 里:

proxy: {
  '/ws': {
    target: 'http://xxx.com',
    ws: true
  }
}

这里的 ws: true 不是"开启 WebSocket 功能",而是:

允许这条代理规则去转发 WebSocket 请求。

也就是这条规则不仅代理 HTTP,也代理 ws。


为什么没开 ws: true,热更新还是正常

因为 Vite 的 HMR WebSocket 是 Vite dev server 自己提供的,不需要你在 proxy 里手动开启。

也就是说:

  • 你没开的是 proxy 是否转发 WebSocket
  • 不是 Vite 自己有没有 WebSocket

所以即使 proxy 里没写 ws: true

  • 浏览器还是会直接连接本地 5173
  • Vite 还是会建立 HMR WebSocket
  • 热更新照样正常

一句话:

ws: true 只影响代理,不影响 Vite 自己的 HMR。


什么时候该开 ws: true

只有一种情况:

你的业务真的有 WebSocket 接口,需要通过 Vite 代理转发。

例如前端会连接:

  • /ws
  • /socket.io
  • /websocket

这时候才应该写:

proxy: {
  '/ws': {
    target: 'ws://backend-server',
    changeOrigin: true,
    ws: true
  }
}

什么时候不该开

下面这些情况一般都不该开:

1. 普通接口代理

比如:

  • /api
  • /gateway

这些是普通 HTTP 请求,不是 WebSocket,不需要 ws: true

2. 页面壳代理

比如你只是想:

  • /xx-ui 走子应用
  • / 走父应用壳

这本质是页面请求代理,也不需要 ws: true

3. 大范围兜底代理

比如:

'^/(?!xx-ui|@vite|src|...)'

这种规则范围很大,如果再开 ws: true,很容易把不该代理的 ws 也带进去。


你的场景里为什么会出问题

你的意图是:

  • /xx-ui 走子应用自己
  • / 代理到父应用壳
  • /api/gateway 走后端

这个思路本身没问题。

问题出在你用了大范围兜底代理,同时开了 ws: true

这样一来,页面加载后,一些 WebSocket 请求也可能命中这条规则,被转发到 target。

而 Vite 开发环境本身就有一条重要的 WebSocket:HMR 热更新通道

你这次其实不是业务 ws 出问题,而是:

Vite 的 HMR WebSocket 被代理规则误伤了。


为什么会一直刷新

因为 HMR 依赖这条 ws 连接。

如果它被代理到错误的 target:

  • 连接失败
  • 不断重连
  • 热更新失效
  • 页面反复 reload

所以现象就是:

  • /xx-ui 首屏能打开
  • 但页面停一会儿就开始一直刷新

这说明问题不是首屏 HTML,而是页面起来后 HMR 的 ws 链路坏了


为什么本地 8080 没问题,线上 target 有问题

不是因为 8080 配对了,而是:

  • 本地环境对错误 ws 更宽容
  • 线上网关 / nginx / 代理更严格
  • 同样的错误配置,线上更容易直接暴露

所以本质上不是"线上有问题",而是:

这条 ws 本来就不该被代理。


正确做法

普通接口单独代理

proxy: {
  '/api': {
    target: 'https://xxx.com',
    changeOrigin: true
  },
  '/gateway': {
    target: 'https://xxx.com',
    changeOrigin: true
  }
}

只有明确业务 ws 才单独开

proxy: {
  '/ws': {
    target: 'wss://xxx.com',
    changeOrigin: true,
    ws: true
  }
}

大兜底规则不要开 ws: true

proxy: {
  '^/(?!xx-ui(/|$)|@vite/|@id/|@fs/|src/|node_modules/|public/)': {
    target: 'http://localhost:8080',
    changeOrigin: true
  }
}

一句话结论

  • ws: true = 这条代理规则也处理 WebSocket
  • 只有明确业务 ws 路径时才开
  • 不要给大范围兜底代理开 ws: true
  • 你这次的问题,本质是 Vite 的 HMR WebSocket 被误代理了
❌
❌