技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTETC

前端性能优化方案

所属馆藏
Other
文件格式
Markdown
原始路径
Other/15-前端性能优化方案
本文目录12 个章节
  1. 一、先理解“性能”
  2. 二、减少不必要的资源和请求
  3. 三、缓存控制
  4. 四、优化资源加载顺序
  5. 五、代码和渲染优化
  6. 六、重定向与连接优化
  7. 七、压缩资源
  8. 八、字体、媒体和资源细节
  9. 九、性能测量和预算
  10. 十、排查清单
  11. 十一、总结
  12. 参考资料

前端性能优化方案

Category(分类): Other Status: 已整理

原文链接:前端性能优化方案

一、先理解“性能”

前端资源包括 HTML、CSS、JavaScript、图片、字体、音视频和其他媒体。性能优化也不只是让首页更快,还包括:

  • 页面何时出现第一块有意义的内容;
  • 最大内容何时完成;
  • 用户点击、输入、滚动时是否及时响应;
  • 页面是否发生意外跳动;
  • 切换路由、打开弹窗和提交表单是否流畅;
  • 资源是否稳定、可缓存、可观察和可回滚。

原文把优化分为页面级和代码级,这个划分仍然有帮助,但今天更推荐先建立基线,再针对瓶颈优化:

  1. 明确设备、网络、页面和用户流程;
  2. 用实验室工具和真实用户监控定位问题;
  3. 找到关键路径上的网络、CPU、渲染或后端瓶颈;
  4. 做一次有针对性的改动;
  5. 重新测量并观察长期趋势。

不要把“减少请求数”当成唯一目标。HTTP/2、HTTP/3 可以复用连接并并发传输,减少请求仍然有价值,但资源总字节、关键请求链、缓存命中率、主线程工作量和服务端响应时间同样重要。

二、减少不必要的资源和请求

1. CSS Sprite:保留场景,不再盲目使用

CSS Sprite 把多个小图片合并成一张图,再通过 background-imagebackground-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 与条件请求

ETagLast-ModifiedIf-None-MatchIf-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. 区分 deferasync

传统的经典脚本会在解析到 <script src> 时暂停 HTML 解析,下载并执行脚本。更稳妥的默认选择通常是:

<script src="/assets/app.js" defer></script>
  • defer 脚本可以并行下载,在文档解析完成后执行,并保持脚本之间的顺序;
  • async 脚本下载完成后立即执行,执行顺序不保证,适合互不依赖的统计或独立脚本;
  • type="module" 脚本默认具有类似延后的行为,但模块内部仍应正确处理依赖和错误;
  • 动态导入适合低频功能,但不应把首屏所需代码全部延后。

把脚本简单地移动到 body 底部是一种历史上的缓解方式,今天更应根据脚本依赖选择 deferasync、模块和动态导入。

3. preloadpreconnectdns-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 应使用与资源类型匹配的 astypecrossorigin 按资源及跨源情况填写,字体预加载通常需要 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,并保留可接受的回退;
  • 为图片和视频声明 widthheight 或使用 aspect-ratio,预留空间避免 CLS;
  • 首屏 LCP 图片不要盲目 loading="lazy",必要时使用 fetchpriority="high"
  • 非首屏图片可使用 loading="lazy",但要结合视口和滚动速度;
  • 上传前压缩图片,避免用大图再通过 CSS 缩小;
  • 不要对已经压缩的 JPEG、WebP、AVIF、MP4 再做无效压缩。

CLS 良好阈值示意

五、代码和渲染优化

INP 交互延迟的三个组成部分

1. CSS Expression 已是历史问题

CSS expression() 是 IE 时代的动态 CSS 表达式,会频繁执行并造成严重性能问题。现代浏览器不应使用它;原文关于避免 CSS Expression 的提醒可以保留,但“如果必须使用”这一建议应删除,因为现代项目没有合理理由继续引入它。

需要动态设置样式时,可以使用 CSS 类、CSS Variables、Web Animations 或经过节流的事件处理程序。不要在每次 scrollmousemove 中无节制地读写布局。

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 上正确配置。永久重定向可以使用 301308,临时策略使用 302307;不能简单宣称“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 路由切换和懒加载内容是否在用户视口中突然出现。

十一、总结

前端性能优化不是把所有图片合成雪碧图、把脚本移到页面底部或把请求数降到最少。更稳妥的顺序是:

  1. 先测量用户真正感知的加载、交互和稳定性;
  2. 优先解决关键路径、服务端响应、资源体积和主线程长任务;
  3. 使用缓存、CDN、压缩、响应式图片和代码分割降低持续成本;
  4. 让构建和 CI 防止体积、指标和依赖风险悄悄回归;
  5. 用 RUM 验证优化是否真的改善了用户,而不是只改善了某一次实验室分数。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS