网络与资源加载优化
从导航关键路径、缓存、资源提示、压缩、CDN 到图片字体和脚本加载,系统化优化网络阶段。
网络与资源加载优化
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-Match、Last-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 缓存失效与回滚
缓存策略要和发布流程一起设计:
- 先上传新 hash 资源并校验可访问;
- 再发布引用新资源的 HTML;
- 保留上一版本资源一段兼容窗口,支持回滚和旧标签页;
- 发生错误时回滚 HTML/路由指向,而不是立即删除旧资源;
- 统计命中率、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 preconnect 和 dns-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 preload、fetchpriority 与 loading 的边界
资源优先级是浏览器调度提示而非强制命令。首屏 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-Encoding 与 Vary: 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 能缩短用户到资源节点的网络距离,但效果依赖缓存命中和回源配置:
- 对不可变 hash 资源设置长缓存,检查 CDN 是否尊重
Cache-Control; - 为 HTML/API 分别设置缓存键、鉴权和
Vary,避免用户数据串缓存; - 配置压缩、HTTP/2/3、TLS 和图片转换,并观察回源率、命中率和错误率;
- 发布时先预热/上传资源,再切换 HTML;保留旧资源支持回滚;
- 第三方脚本和分析域名需评估隐私、CSP、失败隔离和对主链路的影响。
10. 面试追问速答
Q: 文件合并一定能提升性能吗?
A: 不一定。HTTP/1.1 下合并可减少连接和请求调度;HTTP/2/3 的多路复用降低了合并收益。现在更看重首屏关键路径、缓存粒度、压缩和主线程解析成本,应该用瀑布图和现场数据比较。
Q: no-cache 和 no-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 构建流程、前端稳定性。