Chrome DevTools 怎么用?Core Web Vitals 是什么?Lighthouse 怎么看?生产环境怎么监控?
你完全空白没关系,我从打开F12开始教。
在优化之前,你必须知道要优化什么、怎么衡量
想象你打开手机上的一个App:
⏱️ 0-100ms:感觉"瞬间响应" — 就像你刚点开微信,聊天列表就出来了
⏱️ 100-300ms:感觉"很快" — 就像你点开美团,不到半秒看到商家
⏱️ 300-1000ms:感觉"还行" — 就像你打开一个网页,等了不到1秒
⏱️ 1-3秒:感觉"有点慢" — 你会下意识想"是不是卡了"
⏱️ 3秒+:感觉"挂了" — 你会关掉重来或者直接卸载
⏱️ 10秒+:用户直接关页面 — 据Google统计,53%的移动用户会离开加载超过3秒的页面
F12打开的就是它,先认识每个面板干嘛的
不要一上来就全部面板都看。实际工作中,90%的性能问题只需要这三个面板:
1️⃣ Network 面板 — 页面加载慢?先看这里。看哪个资源太大、哪个请求太慢
2️⃣ Performance 面板 — 页面卡顿?录制一段操作,看主线程在干什么
3️⃣ Lighthouse — 不知道从哪开始?先跑一次审计,它会告诉你该优化什么
🖱️ 方式1:右键页面 → "检查"(Inspect)
⌨️ 方式2:F12(Windows)/ Cmd+Option+I(Mac)
⌨️ 方式3:Ctrl+Shift+I(Windows)/ Cmd+Option+I(Mac)
🔍 方式4:Ctrl+Shift+C 直接进入元素选择模式(可以直接点页面上的元素)
页面加载慢,99%的情况先来这里找原因
想象你打开美团外卖首页,页面的各个部分是按顺序加载的:
📄 HTML骨架(搜索栏框架)— 先拿到HTML骨架,浏览器才知道页面结构
🎨 CSS样式(搜索栏变好看了)— 拿到CSS才知道怎么渲染,否则只有裸奔的文字
🔧 JS功能(交互就绪)— JS下载+执行完才能点击、滚动、搜索
🖼️ 图片资源(商家logo、菜品图片)— 不影响操作,但没加载完页面不完整
📡 API数据(商家列表)— 后端返回的动态数据,通常等JS执行完才调接口
瀑布流就是时间线——每个资源什么时候开始请求、下载了多久、什么时候结束。横轴是时间,每个资源一行。
每个请求的耗时拆解(点击请求 → Timing标签):
1. 过滤请求类型:点顶部的 Fetch/XHR 只看API请求,JS 只看JS文件,CSS 只看样式,Img 只看图片
2. 禁用缓存:勾选 Disable cache,模拟首次访问
3. 模拟慢速网络:下拉菜单选 Slow 3G / Fast 3G,模拟移动端弱网环境
4. 查看请求大小:底部状态栏显示总请求数、总传输大小、总加载时间
5. 关注红色请求:失败(4xx/5xx)的请求会显示红色,优先处理
6. 右键请求 → Copy as cURL:可以直接在终端复现这个请求,排查后端问题
页面卡顿、动画掉帧、操作延迟——全靠它排查
Performance 标签Ctrl+E(Mac: Cmd+E)开始录制Screenshots(截图)和 Web Vitals(核心指标)选项。录制时间控制在 5-10 秒,太长数据太多看不清。Bottom-Up 标签能看到具体是哪个函数耗时最长。
1. FPS(每秒帧数):60fps是流畅的底线。低于30fps肉眼就能感觉卡。FPS条绿色=好,红色=差。
2. Main Thread(主线程):JS执行、DOM操作、CSS计算、布局、绘制全在主线程。如果主线程被占满了,用户操作就没响应。
3. Long Task(长任务):任何超过50ms的任务都是长任务。每个长任务都会导致输入延迟(INP变差)。
4. 帧时间线:每一帧(16.6ms)要做的事情:JS → Style → Layout → Paint → Composite。任何一步超时都会掉帧。
Bottom-Up(哪个函数最耗时)和 Call Tree(调用链)Event Log 标签看所有事件的时序
LCP、INP、CLS — 直接影响SEO排名,生产必须关注
想象你打开微信的那一刻:
🕐 FCP(First Contentful Paint) — 你看到顶部"微信"两个字出来的瞬间(可能还只看到骨架屏或者标题)
🕐 LCP(Largest Contentful Paint) — 聊天列表完整显示出来了!白屏→看到内容,你等了多久?1秒觉得正常,3秒觉得慢,5秒以为挂了。最大一块内容渲染出来的瞬间
最大内容通常是:一张大Banner图、一段大标题文字、一个大的内容区块。
LCP 优化方案:
// ✅ preload 关键图片 — 让浏览器提前加载LCP图片
<link rel="preload" as="image" href="/banner.webp" fetchpriority="high">
// ✅ img 标签优化
<img src="/banner.webp"
loading="eager" // LCP图片不要懒加载!
fetchpriority="high" // 高优先级
width="1200" height="400" // 提前声明宽高,避免CLS
decoding="async" // 异步解码
alt="页面Banner">你在微信里点"发送",手指按下去到看到消息气泡出来:
⚡ 好的体验(<200ms):点了立刻看到消息气泡弹出来,感觉"跟手",灵敏
🐢 差的体验(>500ms):点了半天没反应,你以为没发出去又点了一次
INP = 从用户操作(点击/输入/按键)到浏览器完成下一次画面更新的时间
INP 优化方案:
// ✅ 1. 拆分长任务 — yield to main thread
// ❌ 一次性处理1000条数据 → 阻塞主线程200ms
function processData(items) {
items.forEach(item => heavyComputation(item)); // 阻塞!
}
// ✅ 拆成小块,每块之间让出主线程给浏览器渲染
async function processDataChunked(items, chunkSize = 50) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
chunk.forEach(item => heavyComputation(item));
await new Promise(r => setTimeout(r, 0)); // 让出主线程!
}
}
// ✅ 2. 使用 scheduler.yield()(现代浏览器API)
async function handleClick() {
// 先做紧急的UI更新
showLoading();
await scheduler.yield(); // 让浏览器先渲染loading
// 再做重计算
processData(largeDataset);
}
// ✅ 3. Web Worker — 把重计算移到后台线程
const worker = new Worker('heavy-task.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
// 收到结果后更新UI — 主线程全程不阻塞
updateUI(e.data);
};你打开手机网页,看到「限时特价 ¥12」正要点击——突然上面的广告位加载完,页面往下推了一格,你点到了「立即购买 ¥99」!
这就是布局偏移——页面元素在你正要操作的时候突然"跳了"。常见原因:
1. 图片/广告位没有预留空间,加载后把下面的内容挤下去
2. 字体加载慢,先用系统字体渲染,自定义字体加载完文字宽度变了导致跳动
3. 异步插入的DOM元素(如弹窗、广告)把原有内容推开
CLS 优化方案:
// ✅ 1. 图片/视频必须设置宽高 — 提前预留空间
<!-- ❌ 没有宽高,图片加载后布局跳动 -->
<img src="/product.jpg" alt="商品图片">
<!-- ✅ 设置宽高,浏览器提前预留空间 -->
<img src="/product.jpg" width="300" height="200"
style="aspect-ratio: 3/2;" // 用 aspect-ratio 保持比例
alt="商品图片">
// ✅ 2. 字体优化 — font-display
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; // 先用系统字体,加载完再替换(不会跳动)
}
// ✅ 3. 动态内容预留空间
<!-- 广告位预留300x250空间,即使广告没加载也不会跳动 -->
<div style="min-height: 250px;">
<!-- 广告内容异步加载到这里 -->
</div>
// ✅ 4. 使用 CSS contain 属性(现代浏览器)
.card-container {
contain: layout; // 告诉浏览器这个容器内部变化不影响外部布局
}新手的最佳入口,一键生成性能报告+具体建议
Lighthouse 标签(最后面一个💡图标)npx lighthouse https://xxx.com --output html --output-path report.html 自动化跑分
理解重排(Reflow)/重绘(Repaint)/合成(Composite),知道怎么写不卡的代码
| 你改了什么 | 触发的步骤 | 代价 | 例子 |
|---|---|---|---|
| 改位置/大小 | Layout → Paint → Composite | 🔴 最贵 | width/height/top/left/margin |
| 改颜色/背景 | Paint → Composite | 🟡 中等 | color/background/border-color |
| 改 transform/opacity | Composite only | 🟢 最便宜 | transform: translateX(10px) |
will-change 不要滥用!每个 will-change 都会创建一个独立的合成层,太多会消耗大量GPU内存。只在频繁动画的元素上用,动画结束后移除。
重排(Reflow/Layout):你重新布置整个房间——改了元素的宽高位置,所有元素都要重新计算位置。最昂贵的操作。
重绘(Repaint):你只是换了墙纸颜色——改了颜色、背景,位置没变,只需要重新画。中等代价。
合成(Composite):你只是把一张照片移动了一下——只改transform/opacity,不需要重新计算布局。最便宜,GPU直接搞定。
生产经验:用 transform 和 opacity 做动画!这两个属性不触发布局和绘制,GPU直接处理,60fps轻松达到。
图片通常占页面总大小的50-70%,优化图片=优化一半性能
| 格式 | 适用场景 | 压缩率 | 透明度 | 浏览器支持 |
|---|---|---|---|---|
| JPEG | 照片、渐变色图片 | 中 | ❌ | ✅ 全部 |
| PNG | 图标、截图、需要透明 | 低 | ✅ | ✅ 全部 |
| WebP ⭐ | 照片+图标(通用) | 高(比JPEG小30%) | ✅ | ✅ 97%+ |
| AVIF | 最新格式(体积最小) | 极高(比WebP小20%) | ✅ | ⚠️ 92%+ |
| SVG | Logo、图标(矢量图) | 极小 | ✅ | ✅ 全部 |
<picture> 标签
// ✅ 1. 使用 <picture> 标签 — 格式兼容 + 响应式图片
<picture>
<!-- 浏览器按顺序选第一个能识别的格式 -->
<source srcset="/banner.avif" type="image/avif">
<source srcset="/banner.webp" type="image/webp">
<img src="/banner.jpg" alt="页面Banner"
width="1200" height="400"
loading="lazy" // 非首屏图片懒加载
decoding="async"> // 异步解码
</picture>
// ✅ 2. 响应式图片 — 不同屏幕加载不同大小
<img srcset="/product-300.webp 300w,
/product-600.webp 600w,
/product-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
src="/product-600.webp" alt="商品图片">
// 浏览器自动选择最适合当前屏幕大小的图片
// ✅ 3. 图片压缩命令行工具
// 安装: npm install -g sharp-cli
// 批量转WebP:
sharp -i "./images/*.jpg" -f webp -o "./images/webp/" --quality 80
// ✅ 4. Vite构建时自动压缩图片
// npm install vite-plugin-imagemin
import viteImagemin from 'vite-plugin-imagemin'
export default {
plugins: [
viteImagemin({
gifsicle: { optimizationLevel: 7 },
optipng: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
pngquant: { quality: [0.8, 0.9] },
svgo: { plugins: [{ name: 'removeViewBox' }] },
webp: { quality: 80 } // 自动生成WebP版本
})
]
}1. 消除渲染阻塞资源:
// ❌ CSS阻塞渲染 — 浏览器要等CSS下载完才能画任何东西
<link rel="stylesheet" href="/main.css">
// ✅ 非关键CSS异步加载
<link rel="stylesheet" href="/critical.css"> <!-- 首屏关键CSS同步 -->
<link rel="preload" href="/non-critical.css" as="style"
onload="this.onload=null;this.rel='stylesheet'"> <!-- 非关键CSS异步 -->
// ❌ JS阻塞HTML解析
<script src="/vendor.js"></script>
// ✅ 加 defer/async
<script src="/vendor.js" defer></script> <!-- HTML解析完再执行 -->
<script src="/analytics.js" async></script> <!-- 下载完立即执行(不依赖DOM) -->2. prefetch / preload 预加载:
// preload — 当前页面马上要用到的资源,提前下载 <link rel="preload" href="/fonts/custom.woff2" as="font" crossorigin> <link rel="preload" href="/banner.webp" as="image"> // prefetch — 下一页面可能用到的资源,空闲时下载 <link rel="prefetch" href="/admin-page.js" as="script"> // dns-prefetch — 提前解析域名(减少DNS查找时间) <link rel="dns-prefetch" href="//api.example.com"> <link rel="dns-prefetch" href="//cdn.example.com"> // preconnect — 提前建立连接(DNS+TCP+TLS) <link rel="preconnect" href="https://api.example.com">
强缓存 vs 协商缓存 vs Service Worker,面试生产都常考
Cache-Control: max-age=31536000max-age 过期了304 Not Modified200 OK + 新资源// ✅ Nginx 缓存配置(生产最常用)
// 1. 带hash的静态资源(JS/CSS/字体)— 永久缓存
// 文件名如 main.a3f8b2c.js — 改了文件hash就变了,缓存自然失效
location /assets/ {
expires 1y; // 1年强缓存
add_header Cache-Control "public, immutable"; // immutable=永远不验证
// 用户只要不删缓存,永远不发请求!
}
// 2. HTML文件 — 不缓存或短缓存
location / {
add_header Cache-Control "no-cache"; // 每次都协商缓存
// 因为HTML里引用了带hash的JS/CSS路径,HTML变了就会加载新的JS/CSS
}
// 3. API接口 — 不缓存
location /api/ {
add_header Cache-Control "no-store"; // 完全不缓存
// 每次都要拿最新数据
}
// ✅ 协商缓存的两种方式(Nginx自动处理)
// 方式1: ETag(文件内容hash)
// 响应头: ETag: "abc123"
// 下次请求头: If-None-Match: "abc123"
// 服务器比对ETag,一样就返回304
// 方式2: Last-Modified(最后修改时间)
// 响应头: Last-Modified: Thu, 12 Jun 2026 08:00:00 GMT
// 下次请求头: If-Modified-Since: Thu, 12 Jun 2026 08:00:00 GMT
// 服务器比对时间,没改就返回304Cache-Control: max-age=xxx)。就像你手机里的微信聊天记录——上次打开过,这次打开直接显示,完全不用联网。但你朋友发了新消息你看不到,因为是"缓存"ETag / Last-Modified)。就像你打开淘宝,先看看有没有新活动(发个请求问服务器"banner变了吗?"),没变就用本地缓存的,变了就下载新的Lighthouse只在本地跑,真实用户的网络/设备千差万别
// ✅ 使用 web-vitals 库(Google官方出品)
// npm install web-vitals
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
// 采集指标并上报
function reportMetric(metric) {
const { name, value, rating } = metric;
// name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB'
// value: 具体数值(ms或分数)
// rating: 'good' | 'needs-improvement' | 'poor'
// 用 navigator.sendBeacon 上报(页面关闭也能发出去)
const body = JSON.stringify({
name, value, rating,
url: location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
// 可以加自定义维度
userId: getCurrentUserId(),
pageType: 'product-list',
});
navigator.sendBeacon('/api/perf-report', body);
// 或者用 Image 打点(更兼容)
// new Image().src = `/api/perf.gif?name=${name}&value=${value}&rating=${rating}`;
}
// 注册监听
onLCP(reportMetric); // 最大内容渲染
onINP(reportMetric); // 交互延迟
onCLS(reportMetric); // 布局偏移
onFCP(reportMetric); // 首次内容绘制
onTTFB(reportMetric); // 首字节时间// ✅ Sentry 初始化(已有错误监控的项目加几行就行)
import * as Sentry from '@sentry/vue';
Sentry.init({
dsn: 'https://xxx@sentry.io/123',
integrations: [
Sentry.browserTracingIntegration(), // 性能追踪
Sentry.replayIntegration(), // 用户行为回放
],
tracesSampleRate: 0.1, // 采样10%(全采太贵)
// 自动采集: 页面加载、接口请求、用户操作的性能数据
// 设置用户信息
initialScope: {
user: { id: getCurrentUserId() },
tags: { env: 'production' },
},
});
// Sentry 自动采集的指标:
// - Web Vitals (LCP, FID, CLS, TTFB)
// - 接口请求耗时
// - 页面路由切换耗时
// - 长任务 (Long Task)
// - 资源加载失败// ✅ Spring Boot 后端接收性能数据
@RestController
@RequestMapping("/api/perf")
public class PerfReportController {
@Autowired
private PerfMetricRepository repo;
@PostMapping("/report")
public ResponseEntity<Void> report(@RequestBody PerfMetric metric) {
// 存储:指标名、值、评级、URL、UA、时间戳、用户ID
repo.save(metric);
return ResponseEntity.ok().build();
}
}
// ✅ 管理后台查看性能数据
// SQL示例: 查看各页面的LCP P75(第75百分位)— 这才是真实用户体验
// SELECT
// url,
// PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY value) as p75_lcp,
// COUNT(*) as sample_count
// FROM perf_metrics
// WHERE name = 'LCP'
// AND timestamp >= NOW() - INTERVAL '7 days'
// GROUP BY url
// ORDER BY p75_lcp DESC;