性能优化方案
1. 总体描述:从 URL 到页面出现
1.1 简化性能链路
前端性能优化不是从某个单点技巧开始,而是从用户打开页面到页面可交互的完整链路开始看:
输入 URL
-> 命中缓存或发起网络请求
-> DNS / 连接 / CDN 找到资源入口
-> CDN 或服务端返回 HTML
-> 浏览器解析 HTML,发现关键资源
-> 加载 CSS / JS / 图片 / 字体
-> 渲染首屏基础内容
-> 渲染首屏核心内容
-> JS 执行,React 接管页面
-> 页面可交互文字版:
用户输入 URL 后,浏览器会先判断是否可以直接使用缓存;如果不能,就通过 DNS、连接建立和 CDN 找到资源入口,并向 CDN 或服务端请求页面。服务端或 CDN 返回 HTML 后,浏览器开始解析 HTML,在解析过程中发现 CSS、JS、图片、字体等关键资源,并按优先级下载它们。关键资源准备好后,浏览器开始渲染首屏基础内容,也就是页面从白屏变成有内容;随后首屏核心内容,比如主图、标题、列表或主要模块渲染出来。最后,JavaScript 下载并执行,React 完成初始化或 hydration,页面才真正具备点击、输入、滚动等交互能力。
1.2 常见性能指标
指标 | 测量什么 | 适合判断什么问题 | 参考引文 |
|---|---|---|---|
| 从请求开始到收到首字节的时间 | 网络、CDN、服务端响应是否慢 | web.dev 将 TTFB 用作诊断服务端与连接响应速度的基础指标 |
| 页面第一次绘制出文本、图片、SVG 等内容的时间 | 用户是否长时间白屏 | web.dev 将 FCP 定义为首次内容绘制 |
| 首屏视口内最大图片、文本块或视频封面完成渲染的时间 | 首屏主体内容出来得快不快 | web.dev 说明 LCP 衡量加载体验,良好阈值通常是 2.5s 内 |
| 用户点击、输入、键盘交互到下一次页面绘制的延迟 | 页面交互是否卡顿 | web.dev 说明 INP 衡量交互响应,良好阈值通常是 200ms 内 |
| 页面生命周期内意外布局偏移的累计分数 | 页面是否突然跳动、是否容易误触 | web.dev 说明 CLS 衡量视觉稳定性,良好阈值通常是 0.1 内 |
指标和链路的对应关系:
DNS / 连接 / CDN / 服务端返回 HTML
-> TTFB
浏览器解析 HTML,发现并加载关键资源
-> FCP
首屏核心内容渲染完成
-> LCP
JS 执行,React 接管页面,用户点击 / 输入 / 滚动
-> INP
整个展示过程中如果布局突然移动
-> CLS1.3 性能优化层级地图
1. 请求与网络层
解决资源怎么更快到浏览器
2. 资源加载层
解决关键资源怎么更早发现、更快加载、按需加载
3. 页面渲染层
解决资源到了以后,页面怎么更快、更稳定地画出来
4. JS 执行层
解决 JS 解析、执行、主线程阻塞问题
5. React 框架运行层
解决初始化、hydration、组件更新和重渲染成本
6. 交互体验层
解决点击、输入、滚动是否及时响应
7. 监控治理层
解决如何发现问题、验证收益、防止回退2. 请求与网络层优化
目标:减少 DNS、连接、传输、缓存未命中、重定向这些网络链路成本,让 HTML、JS、CSS、图片等资源更快到浏览器。
2.1 CDN 与 HTTP 缓存
为什么要做:
用户如果每次都访问源站,距离远、链路长、源站压力大。CDN 可以让静态资源从离用户更近的边缘节点返回,HTTP 缓存可以让重复访问尽量不再走完整网络链路。
怎么做:
静态资源走 CDN 域名,构建产物文件名带 hash。
JS、CSS、图片等带 hash 资源使用强缓存:
Cache-Control: public, max-age=31536000, immutable。HTML 不做长期强缓存,通常用短缓存或协商缓存,避免发布后用户拿到旧入口。
关注 CDN 命中率,避免缓存规则错误导致大量回源。
避免无意义 query 参数造成 CDN 缓存穿透。
2.2 连接、协议与重定向
为什么要做:
DNS、TCP、TLS、重定向都会产生额外等待。首页如果存在多次 301 / 302,或者关键资源来自多个新域名,都会拖慢首屏。
怎么做:
使用 HTTP/2 或 HTTP/3,利用多路复用和更好的连接能力。
开启 keep-alive,尽量复用连接。
所有入口直接使用最终 URL,减少
http -> https -> www -> /home这类链路。对首屏一定会访问的跨域 CDN 域名使用
preconnect。对可能访问但优先级较低的域名使用
dns-prefetch。
2.3 网络传输压缩
为什么要做:
HTML、CSS、JS、JSON、SVG 都是文本资源,压缩后能显著减少传输体积,弱网下收益明显。
怎么做:
静态构建产物优先预生成 Brotli,gzip 作为兜底。
JS、CSS 这类静态资源可以使用较高 Brotli 等级,因为只压缩一次、多次分发。
SSR HTML、接口 JSON 属于动态响应,压缩等级不要过高,避免压缩 CPU 成本拉高 TTFB。
图片、视频、woff2 等已压缩资源不要再指望 gzip / Brotli 带来明显收益。
典型判断:
如果 TTFB 高,先查 CDN 命中率、重定向链路、连接耗时、服务端响应,而不是先优化 React。
3. 资源加载层优化
目标:浏览器拿到 HTML 后,让首屏关键资源更早被发现、更高优先级加载、更小体积传输,同时把非首屏资源延后。
3.1 打包产物拆分:JS Chunk 与 CSS Chunk
为什么要做:
SPA 如果把所有页面代码都打进一个 main.js,用户访问首页时也会下载详情页、编辑器页、后台页的代码。CSS 也一样,如果所有页面样式都合成一个大 CSS,首页会加载很多当前页面根本用不到的样式。
怎么做:
路由级懒加载:按页面拆 JS chunk。
重组件懒加载:富文本、图表、地图、播放器、代码编辑器等不进入首屏包。
第三方依赖分组:React、router、charts、editor 等按稳定性和体积分组。
CSS code splitting:页面样式随路由加载,重组件样式随组件加载。
保留少量全局样式:reset、变量、基础布局放在全局 CSS,页面私有样式不要全局引入。
Vite 示例:
// vite.config.ts
export default {
build: {
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes("node_modules")) return;
if (id.includes("react") || id.includes("react-dom")) {
return "vendor-react";
}
if (id.includes("react-router")) {
return "vendor-router";
}
if (id.includes("echarts")) {
return "vendor-charts";
}
if (id.includes("@tiptap") || id.includes("prosemirror")) {
return "vendor-editor";
}
return "vendor";
},
},
},
},
};Webpack 示例:
// webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: "vendor-react",
priority: 40,
reuseExistingChunk: true,
},
charts: {
test: /[\\/]node_modules[\\/](echarts|zrender)[\\/]/,
name: "vendor-charts",
priority: 30,
},
editor: {
test: /[\\/]node_modules[\\/](@tiptap|prosemirror)[\\/]/,
name: "vendor-editor",
priority: 30,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
priority: 10,
reuseExistingChunk: true,
},
},
},
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash].css",
chunkFilename: "css/[name].[contenthash].css",
}),
],
};拆分判断标准:
首页 initial JS / CSS 是否明显下降。
编辑器、图表、地图等重依赖是否从首页移出去。
业务代码改动时,vendor chunk 是否能保持缓存稳定。
是否出现过多小 chunk,导致请求调度成本变高。
是否存在公共模块重复打进多个异步 chunk。
3.2 加载策略与优先级治理
为什么要做:
性能问题很多不是资源太大,而是加载顺序错了:非首屏图片抢了首屏图带宽,第三方 SDK 抢了业务 JS,编辑器 chunk 在首页就加载,都会影响 FCP / LCP / INP。
怎么做:
preload:用于当前页面马上要用、但浏览器默认发现较晚的关键资源,例如 LCP 图片、关键字体。
<link rel="preload" href="/images/hero.webp" as="image" />
<link rel="preload" href="/fonts/title.woff2" as="font" type="font/woff2" crossorigin />fetchpriority:用于提高首屏 LCP 图片的下载优先级。
<img
src="/images/banner.webp"
width="750"
height="360"
fetchpriority="high"
decoding="async"
alt=""
/>preconnect:用于首屏一定会访问的跨域 CDN、字体、图片或接口域名。
<link rel="preconnect" href="https://cdn.example.com/" crossorigin />prefetch:用于当前页不急用、下一步大概率会用的路由 chunk。
<link rel="prefetch" href="/assets/detail-page.chunk.js" as="script" />async / defer:业务入口脚本一般用defer,独立第三方脚本可以用async,但非关键第三方脚本更推荐延后到load或 idle 后加载。
<script src="/main.js" defer></script>
<script src="https://third-party.example.com/sdk.js" async></script>可视区触发加载:评论区、推荐区、非首屏图表模块可用
IntersectionObserver在接近可视区时加载。
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
import("./RecommendSection");
observer.disconnect();
}
},
{ rootMargin: "200px" },
);注意点:
LCP 图片不要
loading="lazy"。preload不能滥用,否则会抢占首屏带宽。preconnect通常只给 1-3 个关键域名。第三方脚本默认低优先级,不能进入首屏关键链路。
3.3 资源格式与体积优化
为什么要做:
同样要加载资源,格式和体积直接决定传输时间、解码成本和缓存收益。图片、字体、图标是移动端 H5 中最常见的体积问题来源。
怎么做:
图片:
照片、banner、商品图、游戏封面优先使用 WebP / AVIF。
使用
picture做格式回退。使用
srcset / sizes做响应式图片,移动端不要加载桌面端大图。图片 CDN 按宽度、质量、格式生成多尺寸资源。
非首屏图片使用懒加载,首屏 LCP 图片不要懒加载。
<picture>
<source srcset="/banner.avif" type="image/avif" />
<source srcset="/banner.webp" type="image/webp" />
<img src="/banner.jpg" width="750" height="360" alt="" />
</picture>小图标:
现代项目优先 SVG 或 SVG symbol。
老项目大量 PNG 小图可以使用雪碧图减少请求。
图标库要按需引入,避免把整套 icon 打进首页。
字体:
优先使用
woff2。只加载必要字重。
中文字体尽量使用系统字体,避免加载全量中文字体。
品牌字体做子集化,只覆盖标题或少量字符。
使用
font-display: swap,减少文字不可见时间。
@font-face {
font-family: "BrandFont";
src: url("/fonts/brand-subset.woff2") format("woff2");
font-weight: 600;
font-display: swap;
}压缩等级:
静态 JS / CSS 可预生成高等级 Brotli,因为只压一次、多次分发。
动态 HTML / JSON 使用中等压缩等级,避免 CPU 成本拉高 TTFB。
WebP / AVIF / MP4 / woff2 本身已压缩,不要再依赖 gzip / Brotli。
图片质量不是越低越好,首屏 banner 可从 WebP quality 80-85 起步,缩略图可以更激进。
3.4 第三方与重模块延后
为什么要做:
第三方 SDK 往往体积大、执行不可控,可能抢网络和主线程。富文本、图表、地图、播放器这类重模块也不应该默认进入首屏。
怎么做:
埋点、客服、广告、A/B SDK 在
load后或 idle 阶段加载。监控 SDK 可以较早加载,但要控制体积和采样率。
富文本、图表、地图、视频播放器使用 dynamic import。
用户点击打开重模块时,先给 loading,再加载对应 chunk。
高概率点击模块可以在 hover 或 idle 阶段预取。
const runWhenIdle =
window.requestIdleCallback ||
((cb: IdleRequestCallback) => setTimeout(() => cb({} as IdleDeadline), 2000));
runWhenIdle(() => {
import("./third-party-sdk");
});4. 页面渲染层优化
目标:资源已经到达浏览器后,让页面更快、更稳定地画出来。
4.1 首屏与 LCP 元素治理
为什么要做:
FCP 代表页面开始有内容,LCP 更接近用户认为主体内容出现的时刻。首屏大图、标题、列表主区域如果渲染晚,用户会直接感知页面慢。
怎么做:
明确页面 LCP 元素是什么:主图、标题、视频封面、首屏列表等。
LCP 图片不要 lazy,设置明确宽高或
aspect-ratio。首屏主体内容不要依赖非关键 JS 执行完才显示。
LCP 文本不要被字体加载阻塞太久。
避免 LCP 元素被骨架屏、遮罩、入场动画挡住。
4.2 布局稳定性:CLS 治理
为什么要做:
图片、广告、推荐位、异步模块如果没有占位,加载后会把已有内容顶开,造成布局跳动和误触。
怎么做:
图片、视频、iframe 设置
width / height或aspect-ratio。广告位、推荐位、运营位提前保留空间。
骨架屏尺寸接近真实内容。
不在用户正在阅读的内容上方突然插入模块。
字体 fallback 尺寸尽量接近目标字体。
.cover {
aspect-ratio: 16 / 9;
object-fit: cover;
}4.3 减少 Layout / Paint 成本
为什么要做:
频繁 Layout 会计算元素位置和尺寸,复杂 Paint 会绘制阴影、滤镜、背景等视觉效果。移动端低端机尤其容易卡。
怎么做:
DOM 读写分离,避免强制同步布局。
动画优先使用
transform和opacity。少用大面积
box-shadow、filter、blur、复杂渐变。首屏 DOM 数量要克制,长列表分页或虚拟列表。
非首屏模块可以使用
content-visibility: auto。
.below-fold-section {
content-visibility: auto;
contain-intrinsic-size: 600px;
}5. JS 执行层优化
目标:减少 JS 解析、编译、执行对主线程的占用,让页面更快可交互。
优化点 | 为什么要做 | 简要做法 |
|---|---|---|
减少首屏 JS 体积 | JS 不只是下载,还要解析、编译、执行 | 路由懒加载、组件懒加载、删除无用依赖、拆分重库 |
拆分长任务 | 单个 JS 任务执行太久,会阻塞输入、点击和渲染 | 把大任务拆成小任务,用 |
大计算放 Web Worker | 大量计算放主线程会卡 UI | 搜索、排序、聚合、复杂解析放到 Worker |
延后非关键初始化 | 很多初始化逻辑不是首屏必须 | 埋点、推荐、客服 SDK 在首屏后、 |
优化事件回调 | 点击、输入、滚动事件中执行重逻辑会影响 INP | 回调只做必要更新,重计算延后,高频事件 debounce / throttle |
避免同步大 JSON 处理 | 大 JSON parse、深拷贝、格式化会阻塞主线程 | 服务端裁剪字段,分页加载,增量处理,必要时放 Worker |
减少第三方脚本成本 | 第三方脚本不可控,可能占用主线程 | 延迟加载,限制数量,失败降级,监控第三方耗时 |
使用现代构建产物 | 过度转译会增加体积和执行成本 | 配置 browserslist,减少不必要 polyfill |
一句话总结:JS 执行层优化的核心是减少主线程压力:少执行、晚执行、分片执行、移到 Worker 执行。重点排查首屏大 bundle、长任务、重事件回调、大 JSON 处理和第三方脚本。
6. React 框架运行层优化
目标:减少 React 初始化、hydration、状态更新和组件重渲染带来的额外成本。
优化点 | 为什么要做 | 简要做法 |
|---|---|---|
控制重渲染范围 | 父组件更新会带动子组件重新 render | 状态放在最小影响范围,拆分组件,让更新只影响相关区域 |
使用 | props 没变时不需要重复 render | 用在渲染成本较高、props 相对稳定的组件 |
使用 | 昂贵计算每次 render 都执行会浪费性能 | 缓存过滤、排序、聚合等计算结果 |
使用 | 函数引用变化会导致 memo 子组件重新 render | 给传给子组件的回调保持稳定引用 |
Context 拆分 | 一个大 Context 更新会让所有消费者重新 render | 按业务拆分 Context,区分低频配置和高频状态 |
列表优化 | 大列表一次渲染大量节点会拖慢渲染和交互 | 虚拟列表、分页、item memo、稳定业务 key |
非紧急更新用 | 搜索结果刷新不应该阻塞输入 | 输入状态同步更新,结果列表更新放入 transition |
使用 | 输入联动大列表时,结果更新可以稍微延后 | 保持输入流畅,把重列表渲染延后 |
减少 hydration 成本 | SSR 页面可能“看得到但点不动” | 缩小首屏客户端组件范围,延迟非关键交互模块 |
使用 React Profiler | 盲目 memo 容易无效 | 找 render 次数多、耗时长的组件再优化 |
一句话总结:React 框架层优化的核心是减少无意义渲染和缩小更新范围。优先通过状态设计、组件拆分、列表虚拟化和 Context 拆分解决结构性问题,再用 memo、useMemo、useCallback 做局部优化,最后用 React Profiler 验证效果。
7. 交互体验层优化
目标:用户点击、输入、滚动、拖拽后,页面能及时反馈、不卡顿、不误触。
优化点 | 为什么要做 | 简要做法 |
|---|---|---|
优化点击响应 | 点击后同步执行重逻辑,用户会感觉按钮没反应 | 先给轻量反馈,重逻辑异步处理,避免点击回调大计算 |
优化输入体验 | 输入联动搜索、过滤、大列表渲染容易卡 | 输入状态同步更新,请求 debounce,列表更新用 |
优化滚动性能 | 滚动时执行重逻辑会掉帧 |
|
优化拖拽 / 手势 | 每一帧都计算布局会卡顿 | 使用 |
减少主线程长任务 | 主线程被占用时,所有交互都会延迟 | 拆分长任务,大计算放 Worker,第三方脚本延后 |
避免交互触发整页更新 | 一次点击导致整页更新会拉高 INP | 缩小状态影响范围,局部更新,避免全局 Context 高频更新 |
提前加载交互资源 | 用户点击后才加载大模块,会感觉慢 | 高概率模块 hover / idle 预加载,低概率模块点击后加载并展示 loading |
防止布局跳动和误触 | 交互过程中页面突然移动会造成误点 | 异步内容提前占位,按钮 loading 状态不改变布局 |
弱网交互反馈 | 请求慢时无反馈会导致重复点击 | loading / disabled,请求取消,超时提示,失败重试 |
一句话总结:交互体验层优化的核心是让用户操作后尽快看到反馈。点击和输入要先响应、重逻辑后处理;滚动和拖拽要减少主线程计算;高频事件要节流防抖;资源可以提前预加载;状态更新要控制范围,避免一次交互触发整页重渲染。
8. 监控治理层:以 Sentry 为核心
目标:性能优化不能只靠一次 Lighthouse 跑分,而要形成发现问题、定位原因、验证收益、防止回退的闭环。
8.1 Sentry 接入与自动采集
企业级项目一般不会从零自建完整性能监控平台,而是以 Sentry 作为前端稳定性和性能监控核心。Sentry JavaScript SDK 可以采集页面加载、SPA 路由切换、fetch / XHR、Long Task、Web Vitals 和错误信息,并将性能问题与 release、route、错误栈关联。
基础接入:
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: "xxx",
environment: import.meta.env.MODE,
release: "web@1.2.3",
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 0.1,
});需要关注:
environment区分 production / staging。release绑定版本,方便追踪哪次发布引入性能回退。tracesSampleRate或tracesSampler控制采样率,避免成本过高。Source Map 上传到 Sentry,保证线上错误和性能栈可读。
8.2 自定义业务性能 Span
Sentry 自动采集的是通用指标,业务还需要补充自定义 span。
适合自定义的指标:
搜索结果首次出现时间。
编辑器打开耗时。
AI Chat 首 token 时间。
游戏列表首屏渲染完成时间。
路由 chunk 加载完成时间。
点击按钮到弹窗真正出现的时间。
示例:
const span = Sentry.startInactiveSpan({
name: "editor.open",
op: "ui.action",
});
try {
await openEditor();
} finally {
span.end();
}8.3 上下文、看板与告警
只上报 LCP=4200ms 没有意义,必须带定位上下文:
Sentry.setTag("page_type", "game_home");
Sentry.setTag("webview", "ios_app");
Sentry.setTag("device_level", "low_end");
Sentry.setContext("network", {
effectiveType: navigator.connection?.effectiveType,
});常用维度:
route / page type。
release 版本。
设备类型和设备等级。
网络类型。
浏览器 / WebView / App 版本。
地区。
实验分组。
是否首次访问、是否命中缓存。
看板要能回答:
哪个页面最慢。
哪个版本开始变慢。
是移动端还是桌面端慢。
是 WebView 还是普通浏览器慢。
是资源慢、接口慢、Long Task,还是布局偏移。
告警建议:
核心页面 LCP p75 连续超阈值。
核心页面 INP p75 连续超阈值。
某版本 JS error 或 transaction duration 激增。
某接口 span p95 超过阈值。
某资源加载失败率上升。
8.4 性能预算与发布治理
性能预算是为了防止版本越迭代越慢。
可以设置:
首页 initial JS <= 200KB gzip
单个路由 chunk <= 100KB gzip
首屏图片 <= 150KB
LCP p75 <= 2.5s
INP p75 <= 200ms
CLS p75 <= 0.1
第三方脚本数量 <= N治理流程:
发现问题
-> 判断影响范围
-> 定位页面和版本
-> 找到资源 / 接口 / 交互 / 布局来源
-> 创建优化任务
-> 修复并上线
-> 对比优化前后数据
-> 固化为预算或规则9. 面试高频问题
Q1: 你会如何系统性做前端性能优化?
回答要点:
先从完整链路看:网络、资源加载、页面渲染、JS 执行、React 运行、交互体验、监控治理。
指标上用 FCP 看白屏,LCP 看首屏主体内容,INP 看交互响应,CLS 看布局稳定,TTFB 辅助判断网络和服务端。
优化时先定位瓶颈层,再选择手段,不一上来就套 memo 或压图片。
最后用 Sentry、RUM、性能预算和 release 对比验证收益。
Q2: LCP 慢你会怎么排查?
回答要点:
先确认 LCP 元素是什么,是图片、文本块、视频封面还是首屏列表。
拆成四段看:TTFB、资源发现时间、资源下载耗时、元素渲染延迟。
如果图片发现晚,检查是否藏在 JS 或 CSS background;如果下载慢,做格式、尺寸、CDN 优化;如果渲染晚,查 CSS、字体、主线程阻塞和 hydration。
LCP 图片不要 lazy,必要时使用
preload或fetchpriority="high"。
Q3: 资源加载层你会做哪些优化?
回答要点:
路由懒加载和重组件 dynamic import,减少首屏 JS。
打包工具拆 chunk,React、图表、编辑器等重依赖单独拆,CSS 也按路由和组件拆。
关键资源用 preload、fetchpriority、preconnect;非首屏资源 lazy 或可视区加载。
图片用 WebP / AVIF、响应式图片和 CDN 裁剪;字体用 woff2、子集化和必要字重。
第三方 SDK 延后加载,不进入首屏关键路径。
Q4: React 项目交互卡顿怎么优化?
回答要点:
用 Performance 和 React Profiler 找是主线程长任务、事件回调重,还是组件重渲染。
缩小状态影响范围,拆组件,避免全局 Context 高频更新。
大列表使用虚拟列表,item 用稳定 key,必要时 memo。
输入联动场景用 debounce、
useDeferredValue、startTransition。大计算放 Web Worker,第三方脚本延后。
Q5: 企业级项目怎么做性能监控治理?
回答要点:
以 Sentry 作为稳定性和性能监控核心,采集 Web Vitals、transaction、fetch / XHR、Long Task 和错误。
补充业务自定义 span,例如编辑器打开、搜索结果出现、AI 首 token、游戏列表渲染完成。
上报 route、release、设备、网络、WebView、App 版本等上下文。
配置 Source Map、release 对比、采样率、看板和告警。
用 p75 / p90、性能预算和发布后对比防止回退。


评论
还没有评论
来发表第一条评论吧!