前端性能优化方案
Category(分类): Other Status: 已整理
原文链接:前端性能优化方案
一、先理解“性能”
前端资源包括 HTML、CSS、JavaScript、图片、字体、音视频和其他媒体。性能优化也不只是让首页更快,还包括:
- 页面何时出现第一块有意义的内容;
- 最大内容何时完成;
- 用户点击、输入、滚动时是否及时响应;
- 页面是否发生意外跳动;
- 切换路由、打开弹窗和提交表单是否流畅;
- 资源是否稳定、可缓存、可观察和可回滚。
原文把优化分为页面级和代码级,这个划分仍然有帮助,但今天更推荐先建立基线,再针对瓶颈优化:
- 明确设备、网络、页面和用户流程;
- 用实验室工具和真实用户监控定位问题;
- 找到关键路径上的网络、CPU、渲染或后端瓶颈;
- 做一次有针对性的改动;
- 重新测量并观察长期趋势。
不要把“减少请求数”当成唯一目标。HTTP/2、HTTP/3 可以复用连接并并发传输,减少请求仍然有价值,但资源总字节、关键请求链、缓存命中率、主线程工作量和服务端响应时间同样重要。
二、减少不必要的资源和请求
1. CSS Sprite:保留场景,不再盲目使用
CSS Sprite 把多个小图片合并成一张图,再通过 background-image 和 background-position 显示其中一部分。它在 HTTP/1.1 时代可以减少连接和请求数量,今天在以下场景仍可能有用:
- 一组固定的小型 UI 图标;
- 资源必须保持同一版本;
- 经过实测,单独请求确实造成明显开销。
但在 HTTP/2/HTTP/3、响应式图片和 CDN 场景下,雪碧图也会带来裁剪困难、整张图片一起下载、缓存粒度差和高分辨率适配困难等问题。新的项目通常优先考虑 SVG、独立图标或按需加载的图片,并用真实网络数据决定是否合并。
2. Image maps:语义和可访问性优先
<map> 与 <area> 可以把一张图片划分为多个可点击区域,适合某些地图或示意图。但它不能自动解决键盘导航、焦点、可访问名称和响应式布局问题。普通导航应优先使用语义化链接和按钮,地图应用则应同时提供列表、文本或键盘可操作的替代入口。
3. Data URL:只适合很小且稳定的资源
data: URL 可以把图片或字体嵌入 HTML/CSS,减少独立请求,但会:
- 增大 HTML 或 CSS,Base64 通常还会增加约三分之一体积;
- 失去独立资源的缓存和复用能力;
- 让构建、调试和内容安全策略更复杂。
它可以用于很小的关键图标或由构建工具自动处理的资源,不适合把大图片、字体和常规业务资源全部内联。
4. Font icon:优先评估 SVG
字体图标可以把多个图标打包成字体,支持颜色、大小和旋转。它在旧项目中仍可能存在,但也有语义、无障碍、字体加载、字形替换和像素对齐问题。新项目通常优先使用 SVG 图标或图标组件,并为图标按钮提供可访问名称。
5. 合并文件与代码分割
把多个 CSS 或 JavaScript 合成一个文件在 HTTP/1.1 下可能减少请求,但会降低缓存粒度:一个小改动可能让所有页面重新下载大文件。现代构建工具通常会根据入口、路由和依赖关系做代码分割。
应根据页面访问路径决定:
- 首屏必需代码进入初始包;
- 路由和低频功能使用动态导入;
- 大型编辑器、地图、图表等按需加载;
- 避免把所有第三方依赖打进一个无法缓存和无法按需使用的巨型包;
- 用 Bundle Analyzer 检查重复依赖和意外引入。
三、缓存控制
1. 静态资源使用内容哈希
JS、CSS、图片和字体可以生成内容哈希文件名,例如:
assets/app.8f31c2.js
assets/main.2a4d10.css
内容不变时文件名不变,内容变化时文件名变化。对这类不可变资源可以使用:
Cache-Control: public, max-age=31536000, immutable
HTML 入口和运行时配置通常不能这样处理,因为它们需要尽快发现新的资源清单。常见策略是:
# HTML:允许缓存,但每次使用前验证
Cache-Control: no-cache
# 个性化响应:不允许共享缓存复用
Cache-Control: private, no-cache
# 明确禁止任何缓存保存敏感响应
Cache-Control: no-store
no-cache 的含义是使用前重新验证,并不等于“不保存”;真正禁止存储通常使用 no-store。具体策略应结合响应是否含有个人信息、CDN 行为和 bfcache 需求决定。
2. ETag 与条件请求
ETag、Last-Modified、If-None-Match 和 If-Modified-Since 可以在缓存过期后验证资源是否变化。如果没有变化,服务器返回 304 Not Modified,减少响应体传输。
ETag: "33a64df5"
Cache-Control: max-age=3600
ETag 不是内容哈希缓存的替代方案,也不能解决错误的 CDN 缓存键。分布式服务器生成 ETag 时要注意一致性,个性化响应还要避免被共享缓存错误复用。
3. 外部引用并不总是更快
把 CSS 和 JavaScript 放在外部文件中,能让多个页面共享浏览器缓存,也便于构建和安全策略管理。它会增加关键资源请求,因此要同时考虑:
- 资源是否阻塞首屏;
- 是否命中缓存;
- 是否可以通过 HTTP/2/HTTP/3 复用连接;
- 文件是否过大或被过度拆分;
- 是否需要预加载关键资源。
“全部内联”与“全部外链”都不是普遍答案,应以页面关键路径和真实用户数据为准。
四、优化资源加载顺序
1. CSS 放在 head,并减少渲染阻塞
首屏需要的 CSS 通常应通过 <link rel="stylesheet"> 放在 <head>。把样式表放到文档底部并不能让页面更快,反而可能造成无样式内容闪烁或延迟渲染。
可以进一步做:
- 删除未使用 CSS;
- 按路由或组件拆分样式;
- 对非常小的关键 CSS 进行谨慎内联;
- 避免多层
@import造成串行请求; - 让非关键样式延后加载,但不能为了指标隐藏真实内容。
2. 区分 defer 和 async
传统的经典脚本会在解析到 <script src> 时暂停 HTML 解析,下载并执行脚本。更稳妥的默认选择通常是:
<script src="/assets/app.js" defer></script>
defer脚本可以并行下载,在文档解析完成后执行,并保持脚本之间的顺序;async脚本下载完成后立即执行,执行顺序不保证,适合互不依赖的统计或独立脚本;type="module"脚本默认具有类似延后的行为,但模块内部仍应正确处理依赖和错误;- 动态导入适合低频功能,但不应把首屏所需代码全部延后。
把脚本简单地移动到 body 底部是一种历史上的缓解方式,今天更应根据脚本依赖选择 defer、async、模块和动态导入。
3. preload、preconnect 和 dns-prefetch
资源提示会影响浏览器调度,不是越多越好:
<!-- 只为当前页面很快就要使用的关键字体或图片预加载 -->
<link rel="preload" href="/fonts/app.woff2" as="font" type="font/woff2" crossorigin>
<!-- 对非常关键的跨源连接使用,包含 DNS、TCP 和 TLS 成本 -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<!-- 只提前解析域名,成本低于 preconnect -->
<link rel="dns-prefetch" href="//cdn.example.com">
preload 应使用与资源类型匹配的 as;type 和 crossorigin 按资源及跨源情况填写,字体预加载通常需要 crossorigin。同时要确保资源确实会被使用,否则可能浪费带宽。preconnect 建连成本更高,只对关键跨源 origin 使用。dns-prefetch 只解决 DNS 查询,不能代替连接建立。
原文中的 dns-prefecth 是拼写错误,正确属性是 dns-prefetch。DNS 预解析也不应写成“把结果缓存到系统缓存中”这一绝对说法,最终是否执行和缓存位置由浏览器、操作系统和网络配置决定。
4. 图片加载
图片经常是 LCP 和页面体积的主要来源:
<img
src="/images/hero-800.avif"
srcset="/images/hero-400.avif 400w, /images/hero-800.avif 800w"
sizes="(max-width: 640px) 100vw, 800px"
width="800"
height="450"
fetchpriority="high"
alt="文章封面"
>
建议:
- 根据显示尺寸和 DPR 生成
srcset/sizes; - 对现代浏览器提供 AVIF/WebP,并保留可接受的回退;
- 为图片和视频声明
width、height或使用aspect-ratio,预留空间避免 CLS; - 首屏 LCP 图片不要盲目
loading="lazy",必要时使用fetchpriority="high"; - 非首屏图片可使用
loading="lazy",但要结合视口和滚动速度; - 上传前压缩图片,避免用大图再通过 CSS 缩小;
- 不要对已经压缩的 JPEG、WebP、AVIF、MP4 再做无效压缩。
五、代码和渲染优化
1. CSS Expression 已是历史问题
CSS expression() 是 IE 时代的动态 CSS 表达式,会频繁执行并造成严重性能问题。现代浏览器不应使用它;原文关于避免 CSS Expression 的提醒可以保留,但“如果必须使用”这一建议应删除,因为现代项目没有合理理由继续引入它。
需要动态设置样式时,可以使用 CSS 类、CSS Variables、Web Animations 或经过节流的事件处理程序。不要在每次 scroll、mousemove 中无节制地读写布局。
2. 批量读写 DOM,避免强制同步布局
“每次 DOM 操作都会触发回流”并不准确。浏览器通常会批量处理样式和布局,但以下模式会强迫浏览器立即计算布局:
// 容易造成 layout thrashing:写入后立刻读取布局,再重复写入
for (const item of items) {
item.style.width = `${nextWidth}px`
console.log(item.offsetWidth)
}
更合理的思路是先集中读取,再集中写入:
const widths = items.map(item => item.getBoundingClientRect().width)
requestAnimationFrame(() => {
items.forEach((item, index) => {
item.style.width = `${widths[index] + 10}px`
})
})
DocumentFragment 可以减少把中间状态反复插入文档的次数,但它并不保证所有场景只发生一次布局,也不一定比直接操作更快。真正应通过 Performance 面板和长任务数据验证。
可以进一步使用:
- 通过切换 class 或 CSS Variables 批量更新样式;
content-visibility: auto延后渲染较远的内容;- 虚拟列表减少同时存在和渲染的 DOM;
- Web Worker 承担解析、计算等不需要 DOM 的 CPU 工作;
- 把长任务拆分并适时让出主线程。
不要为了追求速度直接使用未经清洗的 innerHTML 或框架的 HTML 逃生口,性能优化不能取消 XSS 防护。
3. 减少 JavaScript 主线程工作
下载体积只是第一步,JavaScript 还要解析、编译和执行。可以:
- 删除未使用代码和重复依赖;
- 使用现代浏览器目标减少不必要的 polyfill;
- 将低频功能动态导入;
- 减少启动阶段的初始化和同步数据处理;
- 把大计算移到 Worker;
- 避免在事件回调中同步处理大量 DOM;
- 使用
requestIdleCallback或调度器处理非关键工作,并提供不支持时的回退; - 通过 Performance 面板定位 Long Task,而不是只看压缩后的 KB。
六、重定向与连接优化
1. 减少不必要的重定向
HTML 入口前的重定向会延迟关键资源发现,重定向链还会额外增加网络往返。应直接使用最终 URL、合并重定向链,并避免页面加载后再用 JavaScript 跳转。
HTTP 到 HTTPS 的升级应在服务器和 CDN 上正确配置。永久重定向可以使用 301 或 308,临时策略使用 302 或 307;不能简单宣称“301 会被浏览器永远记住”,缓存时间、浏览器策略和 HSTS 都会影响行为。对于已稳定使用 HTTPS 的站点,可以评估 HSTS,但必须先确认所有子域和资源都支持 HTTPS。
2. CDN、HTTP/2 和 HTTP/3
CDN 通过边缘节点缓存和传输静态资源,通常可以缩短用户到资源的网络距离,但“自动选择跳数最少或最快服务器”不是所有 CDN 的固定保证。效果取决于节点覆盖、路由、缓存命中率、回源距离、缓存键和资源发布策略。
部署 CDN 时应关注:
- 静态资源内容哈希和缓存失效;
- HTML、API 和个性化响应的缓存边界;
Vary、压缩、跨域和 CORS;- HTTPS、证书、源站保护和访问控制;
- 命中率、回源率、错误率和不同地区的真实体验。
HTTP/2/HTTP/3 能减少连接建立和队头阻塞的一部分成本,但不能弥补过大的 JS、低缓存命中率或慢服务端响应。
七、压缩资源
1. 内容编码
浏览器会通过 Accept-Encoding 声明支持的压缩算法,服务器再用 Content-Encoding 告知实际编码:
Accept-Encoding: gzip, br, zstd
Content-Encoding: br
Brotli(br)通常适合 HTTPS 下的文本资源,Gzip 仍是重要回退。Zstandard(zstd)的浏览器和服务器支持应按目标用户与基础设施确认,不能只因为某个环境支持就默认所有用户可用。
不要对已经压缩的图片、音视频和压缩包重复压缩。压缩策略还要考虑 CPU 消耗、动态响应缓存和内容类型。
2. 代码最小化
生产构建通常会删除注释和无意义空白、压缩变量、移除死代码并生成 source map。Source map 适合上传到受控的错误监控平台,不应无条件公开,因为它可能暴露源码和目录信息。
3. HTML 流式输出
原文提到后端使用 flush() 尽早发送已经生成的 HTML。这个方向今天可以扩展为流式 SSR、渐进式 HTML 和 HTTP 早期提示,但是否真正提前到达浏览器取决于应用服务器、反向代理、CDN、HTTP 版本和缓冲配置。
流式输出只有在首屏 HTML 可以分段生成时才有价值;如果后端必须等待全部数据,应该先优化 TTFB、数据库和接口依赖,不要把一个不能真正刷新的 flush() 当成性能方案。
八、字体、媒体和资源细节
- 字体优先使用 WOFF2,按语言或字重拆分并避免加载未使用字重;
- 对关键字体谨慎使用
preload,并设置正确的crossorigin; - 使用
font-display控制字体加载期间的显示行为; - 视频提供合适的封面、尺寸和播放策略,避免首屏自动加载大媒体;
- 大型音视频使用流式或分段传输,不要把整个文件打进前端包;
- SVG 可以缩小体积,但插入用户可控 SVG 时必须进行安全清洗;
- 资源 URL、缓存键和版本策略要与部署方式保持一致。
九、性能测量和预算
1. 实验室数据与真实用户数据
Lighthouse、Chrome DevTools 和 WebPageTest 适合在开发和 CI 中使用固定设备、网络和脚本复现问题。它们不能代表所有真实用户。
RUM(Real User Monitoring)收集真实设备、网络、地区、浏览器和用户操作下的数据,更适合判断线上体验和回归趋势。实验室指标与 RUM 不一致并不一定是工具错误,尤其是 CLS、INP 等会受到页面生命周期和用户交互影响。
2. 建立预算
可以为不同页面设置:
- 首屏 JavaScript/CSS/字体/图片的字节预算;
- 初始请求数和关键请求链长度;
- LCP、INP、CLS、FCP、TTFB 的目标;
- 长任务数量和最大主线程阻塞时间;
- 构建时间、产物体积和第三方脚本数量。
预算超标时让 CI 告警或阻断,并在 PR 中说明原因。不要只追求单次 Lighthouse 满分,用户体验、可访问性、功能完整性和数据准确性同样重要。
十、排查清单
加载慢
- TTFB 是否过高;
- HTML 是否被缓存或压缩;
- LCP 元素是什么,是否被 CSS、字体或 JS 延迟;
- 首屏图片是否过大、格式不合适或错误地 lazy-load;
- 是否存在过长的关键请求链和不必要重定向;
- CDN 是否命中,缓存键是否正确;
- 初始 JavaScript 是否过大,解析/执行是否产生长任务。
页面卡顿
- Performance 面板中是否有 Long Task;
- 事件回调是否做了过多计算或 DOM 更新;
- 是否发生 layout thrashing;
- 是否需要虚拟列表、Worker 或分片处理;
- 第三方脚本是否占用主线程;
- 低端设备和真实移动网络是否复现问题。
页面跳动
- 图片、视频、广告和 iframe 是否声明尺寸;
- 异步插入内容是否预留空间;
- 字体切换是否造成明显位移;
- 动画是否使用
transform等更合适的属性; - SPA 路由切换和懒加载内容是否在用户视口中突然出现。
十一、总结
前端性能优化不是把所有图片合成雪碧图、把脚本移到页面底部或把请求数降到最少。更稳妥的顺序是:
- 先测量用户真正感知的加载、交互和稳定性;
- 优先解决关键路径、服务端响应、资源体积和主线程长任务;
- 使用缓存、CDN、压缩、响应式图片和代码分割降低持续成本;
- 让构建和 CI 防止体积、指标和依赖风险悄悄回归;
- 用 RUM 验证优化是否真的改善了用户,而不是只改善了某一次实验室分数。