跳到正文
前端知识库
性能

网络与资源加载优化

从导航关键路径、缓存、资源提示、压缩、CDN 到图片字体和脚本加载,系统化优化网络阶段。

8 分钟HTTP · 缓存 · CDN · preload · prefetch · Service Worker · 图片 · 字体

网络与资源加载优化

1. 从 URL 到首屏的关键路径

导航可以拆为:

URL/重定向 -> DNS -> TCP/QUIC -> TLS -> HTTP 请求
  -> HTML 下载与解析 -> CSSOM/DOM -> 样式/布局
  -> 关键资源下载、执行和绘制 -> LCP

优化时先问“哪一段在关键路径上”,再选择手段。比如 DNS 预取只减少域名解析等待,不能修复服务端 TTFB;压缩只减少传输字节,不能修复主线程解码和执行;把脚本改成 defer 也不能替代代码分割。

2. HTTP 缓存设计

2.1 强缓存与协商缓存

类型 浏览器行为 常见响应头 适合
强缓存 在新鲜期直接使用本地响应,不发起验证请求 Cache-Control: max-age=...、历史 Expires 带内容指纹的 JS/CSS/字体/图片
协商缓存 发送条件请求,资源未变化时服务端返回 304 ETag/If-None-MatchLast-Modified/If-Modified-Since HTML、需要及时验证的资源
Service Worker 缓存 请求先经过 SW,可自定义缓存/网络策略 Cache Storage API 离线、预缓存、细粒度降级

Cache-Control 优先于 Expires。常见指令含义:

  • no-store:不要存储响应,适用于高度敏感或必须每次获取的数据;
  • no-cache:可以存储,但使用前必须向服务端重新验证,不等于“不缓存”;
  • public:允许共享缓存(浏览器、CDN 等)保存;
  • private:只允许用户代理私有缓存,不应放入共享缓存;
  • immutable:在新鲜期内声明资源不会变化,适合文件名带 hash 的静态文件;
  • stale-while-revalidate:允许短时间使用旧值并在后台更新,需结合业务一致性评估。

推荐的静态资源策略:

# app.8f31c.js、style.8f31c.css 等内容指纹文件
Cache-Control: public, max-age=31536000, immutable

# HTML 入口,文件名不带 hash
Cache-Control: no-cache
ETag: "release-2026-08-27"

HTML 入口通常短缓存或协商缓存,保证新版本能发现;带 hash 的静态资源可以长缓存。发布时必须保证 HTML 引用的资源已经可用,避免新 HTML 先到而 chunk 尚未上传。

2.2 ETag、Last-Modified 和多节点部署

  • ETag 可精确表示实体版本,但生成算法必须在节点间稳定;不要因为“分布式部署”就机械关闭它。
  • Last-Modified 精度通常为秒,短时间内多次发布可能无法区分,适合作为补充校验。
  • CDN/反向代理的缓存键、压缩变体和 Vary(例如 Accept-Encoding)必须与源站策略一致。
  • 不要把用户私有数据用 public 缓存;含 Cookie、Authorization 或个性化内容的响应需明确边界。

2.3 缓存失效与回滚

缓存策略要和发布流程一起设计:

  1. 先上传新 hash 资源并校验可访问;
  2. 再发布引用新资源的 HTML;
  3. 保留上一版本资源一段兼容窗口,支持回滚和旧标签页;
  4. 发生错误时回滚 HTML/路由指向,而不是立即删除旧资源;
  5. 统计命中率、304 比例和 chunk 加载失败率。

3. Service Worker

Service Worker 在受控页面与网络之间提供可编程拦截层,运行在独立上下文、不能直接访问 DOM。它不应被当成“把所有资源都缓存”的开关;缓存版本、更新和失效策略必须明确。

const CACHE = 'app-shell-v3'
const SHELL = ['/', '/index.html', '/assets/app.8f31c.js']

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)))
})

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) => Promise.all(
      keys.filter((key) => key !== CACHE).map((key) => caches.delete(key)),
    )),
  )
})

self.addEventListener('fetch', (event) => {
  const request = event.request
  if (request.method !== 'GET') return

  // 导航通常网络优先,失败再回退到离线壳;API 不要盲目缓存。
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request).catch(async () => {
        const fallback = await caches.match('/index.html')
        return fallback ?? new Response('Offline', {
          status: 503,
          headers: { 'Content-Type': 'text/plain; charset=utf-8' },
        })
      }),
    )
    return
  }

  event.respondWith(caches.match(request).then((cached) => cached || fetch(request)))
})

更新流程要考虑旧页面仍由旧 SW 控制的情况。skipWaiting()clientsClaim() 能加速接管,但可能让同一页面的资源版本不一致;大型应用应设计版本握手和刷新提示。缓存失败、配额不足和隐私数据都应有降级路径。

4. 资源提示和请求优先级

4.1 preload

preload 提高指定资源的加载优先级,必须与最终使用方式、as、CORS 属性一致:

<link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image" fetchpriority="high">

只预加载当前页面确定会用到且位于关键路径的资源。预加载过多资源会竞争 HTML、CSS 和 LCP,甚至比不预加载更慢。

4.2 prefetch

prefetch 是低优先级的未来导航提示,适合用户下一步很可能访问的路由或资源;弱网、数据节省模式和移动设备上应允许浏览器忽略它。它不应替代当前页面的关键资源加载。

4.3 preconnectdns-prefetch

<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="dns-prefetch" href="//analytics.example.com">

preconnect 可能建立 DNS、TCP 和 TLS 连接,适合少数确定会访问的跨源;dns-prefetch 只做 DNS,成本更低。通常只对 2-4 个关键域名使用,并检查实际连接是否被复用。

4.4 preloadfetchpriorityloading 的边界

资源优先级是浏览器调度提示而非强制命令。首屏 LCP 图片可结合明确尺寸、fetchpriority="high" 和不懒加载;首屏以下图片可使用 loading="lazy"。同一资源不要同时堆叠多个互相冲突的提示。

5. HTTP/1.1、HTTP/2 和 HTTP/3

  • HTTP/1.1 对同一主机并发连接有限,过去常用域名分片和文件合并;域名分片会增加 DNS/TLS/连接成本。
  • HTTP/2 在一条连接上多路复用并压缩头部,通常应收敛静态资源域名,避免盲目分片;仍需关注队头阻塞和服务器调度。
  • HTTP/3 基于 QUIC/UDP,连接迁移和丢包恢复对移动网络可能更友好,但需要 CDN、网关和观测链路完整支持。
  • HTTP/2/3 不意味着“请求越多越好”:请求头、解析、解码和主线程调度仍有成本;按缓存粒度和关键路径决定是否合并。

6. 压缩与传输

6.1 文本压缩

服务端或 CDN 应为 HTML、CSS、JS、JSON、SVG 等文本启用 Brotli(优先)或 gzip,并返回正确的 Content-EncodingVary: Accept-Encoding。压缩级别越高 CPU 成本越大,需对构建时预压缩和实时压缩分别评估。

6.2 构建压缩

// webpack 生产配置示意
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')

module.exports = {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: { compress: { drop_console: false } },
      }),
      new CssMinimizerPlugin(),
    ],
  },
}

是否删除 console 应按日志治理决定,不要为了体积破坏线上诊断。压缩后保留受控 Source Map,并确保 release 与 map 一一对应。

7. 图片和字体

7.1 响应式图片

<picture>
  <source type="image/avif" srcset="hero-640.avif 640w, hero-1280.avif 1280w" sizes="100vw">
  <source type="image/webp" srcset="hero-640.webp 640w, hero-1280.webp 1280w" sizes="100vw">
  <img
    src="hero-1280.jpg"
    width="1280"
    height="720"
    alt="产品概览"
    decoding="async"
  >
</picture>

宽高或 aspect-ratio 预留空间可降低 CLS。根据真实设备 DPR、视口和内容重要性选择候选尺寸;不要把原图缩放后交给浏览器下载。

7.2 懒加载与预加载

非首屏图片使用原生 loading="lazy" 或 IntersectionObserver;首屏 LCP 图片通常不应懒加载。占位图、模糊预览和骨架屏用于改善感知,但不能掩盖真实资源失败。

7.3 字体

优先使用 WOFF2,裁剪字符集,预加载首屏确实使用的字体,并设置 font-display

@font-face {
  font-family: AppSans;
  src: url('/fonts/app-sans-latin.woff2') format('woff2');
  font-display: swap;
  font-weight: 400;
}

自定义字体可能引起 FOIT/FOUT 和布局偏移;为关键文本准备度量接近的后备字体,并测量字体交换对 CLS/LCP 的影响。

8. 脚本加载与资源拆分

<!-- 无依赖的第三方统计脚本 -->
<script async src="https://analytics.example.com/sdk.js"></script>

<!-- 有顺序依赖的业务脚本 -->
<script defer src="/assets/runtime.js"></script>
<script defer src="/assets/app.js"></script>
  • 普通 <script> 会暂停 HTML 解析并在执行时阻塞主线程;
  • async 并行下载,下载完成立即执行,多个脚本顺序不保证;
  • defer 并行下载,在 HTML 解析完成后按声明顺序执行;
  • 动态创建脚本默认异步;
  • type="module" 默认延迟执行并有模块依赖语义,但仍需关注模块图和执行成本。

完整加载机制见 JavaScript 加载机制

9. CDN 与边缘策略

CDN 能缩短用户到资源节点的网络距离,但效果依赖缓存命中和回源配置:

  1. 对不可变 hash 资源设置长缓存,检查 CDN 是否尊重 Cache-Control
  2. 为 HTML/API 分别设置缓存键、鉴权和 Vary,避免用户数据串缓存;
  3. 配置压缩、HTTP/2/3、TLS 和图片转换,并观察回源率、命中率和错误率;
  4. 发布时先预热/上传资源,再切换 HTML;保留旧资源支持回滚;
  5. 第三方脚本和分析域名需评估隐私、CSP、失败隔离和对主链路的影响。

10. 面试追问速答

Q: 文件合并一定能提升性能吗?

A: 不一定。HTTP/1.1 下合并可减少连接和请求调度;HTTP/2/3 的多路复用降低了合并收益。现在更看重首屏关键路径、缓存粒度、压缩和主线程解析成本,应该用瀑布图和现场数据比较。

Q: no-cacheno-store 有什么区别?

A: no-store 禁止存储;no-cache 允许存储但使用前必须验证。把 no-cache 说成“不缓存”是常见误区。

Q: 为什么加了 preload 反而变慢?

A: 预加载会提高优先级并占用连接/带宽。如果预加载了非关键或不会使用的资源,就会和 HTML、CSS、LCP 资源竞争;还可能因 as/CORS 不匹配造成重复请求。

Q: Service Worker 适合缓存 API 吗?

A: 要按数据新鲜度、隐私和一致性设计。静态壳可缓存优先,导航常用网络优先并离线回退;带用户数据的 API 不能未经审查写入共享缓存。

11. 资料来源与现有专题

本篇综合三份性能 PDF 关于 HTTP 缓存、Service Worker、资源压缩、DNS 预取/预连接、CDN、图片懒加载、脚本和 HTTP/2/3 的内容,并补充现代浏览器的优先级和缓存边界。相关专题:指标采集与问题诊断CSS 性能优化Webpack 构建流程前端稳定性