跳到主要内容

网络与资源加载优化

问题

如何从 HTML、HTTP 缓存、资源优先级、CDN、压缩和数据请求等层面系统优化页面加载?

面试速答版

网络优化不是“把所有资源都 preload”。先看请求瀑布,找出关键资源什么时候被发现、为什么排队、下载多久、是否阻塞渲染:

  • HTML 要尽快返回,文本资源使用 Brotli/gzip,静态资源部署到合适的 CDN。
  • 带内容哈希的 JS/CSS/图片可长期缓存并设 immutable;HTML 通常短缓存或协商更新,避免新旧版本错配。
  • LCP 图片应尽量在初始 HTML 可发现,不要懒加载;必要时对唯一高概率 LCP 使用 fetchpriority="high",CSS 背景图等晚发现资源再考虑 preload。
  • preconnectpreloadprefetch 都会消耗连接或带宽,只给有数据证明的关键资源使用。
  • HTTP/2/3 下不要继续做大量域名分片;连接复用、缓存命中和优先级通常比“请求数量越少越好”更重要。
  • 数据请求避免串行 waterfall,能并行就并行;服务端用 Server-Timing 暴露网关、缓存、数据库等阶段。

一、先读懂资源加载链路

排查时不要只看 duration,还要回答:

  1. 资源是否足够早被发现?
  2. 浏览器是否给了合适的优先级?
  3. 是否在等待连接、缓存验证或更高优先级资源?
  4. 下载慢是文件大、网络慢还是 CDN 节点慢?
  5. 下载后是否还有解析、执行、字体整形或图片解码成本?

二、HTML 与 TTFB

TTFB 高可能来自重定向、CDN 回源、应用渲染、数据库、第三方接口或冷启动。只说“加 CDN”不足以定位。

navigation-timing.ts
const navigation = performance.getEntriesByType(
'navigation',
)[0] as PerformanceNavigationTiming | undefined;

if (navigation) {
console.table({
redirect: navigation.redirectEnd - navigation.redirectStart,
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
connect: navigation.connectEnd - navigation.connectStart,
tls: navigation.secureConnectionStart > 0
? navigation.connectEnd - navigation.secureConnectionStart
: 0,
ttfb: navigation.responseStart - navigation.startTime,
downloadHtml: navigation.responseEnd - navigation.responseStart,
});
}

服务端优化方向:

  • 减少重定向和跨地域回源。
  • 数据请求并行化,避免服务端 waterfall。
  • 为公共 HTML 或 RSC 结果设计可验证的边缘缓存。
  • 缓慢依赖设置超时、fallback 和熔断,不让一个非关键模块阻塞整页。
  • 能流式返回时先发送静态壳与关键内容,不必等所有区域完成。

三、HTTP 缓存与版本一致性

3.1 推荐按资源类型制定策略

资源常见策略原因
HTMLno-cache 或短 max-age + revalidate及时获得新资源映射和页面内容
带内容哈希 JS/CSSpublic, max-age=31536000, immutableURL 变化即新版本,可长期缓存
图片/字体内容哈希或稳定版本 URL + 长缓存复用率高,减少重复传输
用户私有接口private/no-store 或明确的客户端缓存避免公共缓存串用户
可共享公共接口s-maxage、ETag、stale-while-revalidate由业务新鲜度决定
# 内容哈希静态资源:例如 app.a1b2c3.js
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# HTML:允许缓存但每次使用前验证;也可按业务设置短 max-age
location = /index.html {
add_header Cache-Control "no-cache";
}
no-cache 不等于“不存储”

no-cache 允许缓存,但复用前要向服务器验证;no-store 才是不应存储。对所有 HTML 一律 no-store 会失去验证缓存能力,对敏感内容又不能只用 no-cache,必须根据数据性质选择。

3.2 防止部署后白屏

  • HTML 和静态资源不要使用同一个永久 Cache First 策略。
  • 新版本上线后保留旧 hash 资源一段时间,覆盖已打开标签和渐进发布窗口。
  • 动态 import 失败时识别 chunk/version 错误,受控刷新一次并防止循环。
  • Service Worker 激活、缓存清理和客户端接管要有明确版本协议。
  • 发布系统应验证 HTML 引用的所有资源在 CDN 上真实存在后再切流。

四、压缩与传输体积

HTML、CSS、JavaScript、JSON 和 SVG 等文本资源应使用 Brotli 或 gzip。Brotli 常比 gzip 进一步减少约 15%–20%,但实际收益取决于内容和压缩级别;构建期高压缩等级适合静态资源,动态响应要平衡 CPU 延迟。

需要区分三种体积:

  • 源文件大小:开发源码,不代表用户下载量。
  • 压缩传输大小:Network 中的 transferred,适合网络预算。
  • 解压/解析后大小:影响 JS parse/compile、内存和执行成本。

一个 200KB gzip 的 JavaScript 在浏览器中可能展开为更大的源码和对象,因此“网络很快”不代表主线程也快。

五、资源发现与优先级

5.1 资源提示对比

能力做什么适用场景主要风险
dns-prefetch提前 DNS 查询低概率但可能访问的跨域收益有限
preconnect提前 DNS/TCP/TLS很快会用到的关键跨域连接和 socket 浪费
preload强制提前请求当前页资源晚发现但确定需要的关键资源抢带宽、重复下载
prefetch低优先级获取未来资源高概率下一步导航用户未导航时浪费流量
fetchpriority提示相对请求优先级高概率 LCP 图、低优先级缩略图设置太多后失去区分度
<!-- 关键跨域图片 CDN:只对真正关键的 origin 预连接 -->
<link rel="preconnect" href="https://img.example.com" crossorigin />

<!-- CSS 背景 LCP 图在外部 CSS 中才会被发现,可选择性预加载 -->
<link
rel="preload"
as="image"
href="/hero-1280.avif"
fetchpriority="high"
/>

5.2 响应式图片预加载

直接 preload 一个固定 href 可能让小屏设备下载错误尺寸。响应式图片应带 imagesrcsetimagesizes

<link
rel="preload"
as="image"
imagesrcset="/hero-640.avif 640w, /hero-1280.avif 1280w"
imagesizes="100vw"
fetchpriority="high"
/>

<img
src="/hero-640.avif"
srcset="/hero-640.avif 640w, /hero-1280.avif 1280w"
sizes="100vw"
width="1280"
height="720"
fetchpriority="high"
alt="产品主视觉"
/>

通常一个页面只给 1 个、最多极少数高概率 LCP 资源设置 high。首屏以外图片使用 loading="lazy",LCP 图不要懒加载。

5.3 JavaScript 加载顺序

写法HTML 解析执行顺序适用场景
普通 script阻塞立即极少数必须同步的启动脚本
defer并行下载DOM 解析后按文档顺序传统应用主脚本
async并行下载下载完立即执行,顺序不保证独立第三方脚本
type="module"默认 defer 语义按模块依赖执行现代 ESM 应用

动态 import 能减少首屏 JavaScript,但过度拆分会增加请求、模块调度、运行时和缓存失效成本。应按路由、重功能和稳定依赖边界分包,而不是“一组件一 chunk”。

六、LCP 资源加载

LCP 可以拆成四段:

阶段优化问题
TTFBHTML 为什么还没到?
Resource Load DelayLCP 资源为什么这么晚才开始?
Resource Load Duration文件为什么下载这么久?
Element Render Delay文件到了,为什么还没画出来?

常见反模式:

  • Hero 图片由客户端 JavaScript 请求数据后才创建。
  • LCP 图写在外部 CSS 背景中,却没有必要的 preload。
  • LCP 图错误设置 loading="lazy"
  • 同时 preload 多张轮播图、所有字体和大量 chunk,关键图反而排队。
  • 图片下载很快,但主线程被 Hydration 或第三方脚本阻塞,render delay 很高。

七、HTTP/2、HTTP/3 与连接复用

现代协议支持同连接多路复用,不应继续机械应用 HTTP/1.1 时代的域名分片和超大雪碧图策略。

  • 同源或少量稳定 CDN origin 更容易复用连接、TLS 和拥塞窗口。
  • HTTP/2 多路复用仍会受服务端优先级、带宽竞争和 TCP 丢包影响。
  • HTTP/3 使用 QUIC,改善连接建立和部分丢包场景,但不会让大 JS 免于解析执行。
  • 合并资源和拆分资源都要围绕缓存、关键路径和运行时成本做权衡。

八、数据请求与瀑布流

8.1 能并行的请求不要串行

// ❌ 无依赖却串行
const profile = await fetchProfile();
const recommendations = await fetchRecommendations();

// ✅ 同时启动
const [profile, recommendations] = await Promise.all([
fetchProfile(),
fetchRecommendations(),
]);

有依赖的请求可以考虑:

  • 后端聚合/BFF,减少客户端往返。
  • 服务端在同一数据中心并行取数。
  • 将稳定公共数据缓存到边缘节点。
  • 流式返回先到的区域,而不是等待全部数据。
  • 对搜索、自动补全等过期请求使用 AbortController。

8.2 重试、超时和幂等

网络优化不能靠无限重试“提高成功率”。

async function fetchWithTimeout(
input: RequestInfo | URL,
timeoutMs = 5_000,
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort('timeout'), timeoutMs);

try {
return await fetch(input, { signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}

这里的 5 秒是假设普通 API 的示例值;支付、搜索、报表和大模型请求应使用不同超时。只对网络错误、429、部分 5xx 等可恢复错误按策略重试,并使用指数退避与抖动;写请求必须确认幂等性。

九、第三方资源治理

第三方脚本既消耗网络,也可能在下载后产生长任务:

  • 列出每个第三方的业务负责人、加载页面、触发条件、体积和 CPU 时间。
  • 同意管理、广告和客服脚本按用户授权与页面需要加载。
  • async/defer 只影响加载/执行顺序,不会减少脚本执行成本。
  • 对供应商做 request blocking 实验,比较 LCP、TBT、INP 和业务指标。
  • 自托管可能改善连接与缓存,也会承担更新、安全和合规责任,不能一概而论。

十、让前后端耗时可以关联

Server-Timing: edge;dur=18, cache;desc="MISS";dur=3, app;dur=95, db;dur=41
Timing-Allow-Origin: https://www.example.com
const navigation = performance.getEntriesByType(
'navigation',
)[0] as PerformanceNavigationTiming;

for (const metric of navigation.serverTiming) {
console.log(metric.name, metric.duration, metric.description);
}

跨域 Resource Timing 需要资源响应提供 Timing-Allow-Origin,否则部分阶段和传输大小会显示为 0。这个 0 不能直接当作缓存命中或请求失败。

十一、资源加载检查清单

  • HTML TTFB 已按边缘、应用、数据库拆解
  • HTML、hash 静态资源、API 使用不同缓存策略
  • 发布流程能防止 HTML 与 chunk 版本错配
  • 文本资源已压缩,并同时查看传输和解压后体积
  • LCP 资源在初始 HTML 可发现且没有懒加载
  • preload/preconnect/fetchpriority 数量少且有瀑布图证据
  • 数据请求没有无意义的串行 waterfall
  • 第三方脚本有负责人和网络/CPU 预算
  • Server-Timing 与 traceId 能关联后端阶段
  • 优化在冷/热缓存、不同网络和真实用户数据中验证

常见面试问题

Q1: preload 和 fetchpriority 有什么区别?

答案

preload 解决“浏览器发现得太晚”,会强制提前发起请求;fetchpriority 只提示同类资源的相对优先级,不负责让浏览器提前发现。CSS 背景 LCP 图可能两者一起用;初始 HTML 中已有的 img 通常先评估只加 fetchpriority="high"

Q2: 为什么不能把所有首屏资源都 preload?

答案

preload 会抢占连接和带宽。预加载字体、图片和 chunk 太多时,真正的 LCP 图、CSS 或主脚本反而排队;未使用的 preload 还会浪费流量。应只用于晚发现且当前页面确定需要的少量关键资源。

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

答案

no-cache 允许存储,但每次复用前必须验证;no-store 要求缓存不要存储响应。频繁更新但不敏感的 HTML可使用 no-cache,敏感私有数据才考虑 no-store,并结合 private、鉴权和代理缓存规则。

Q4: 为什么带 hash 的静态资源可以缓存一年?

答案

内容改变后 URL 也改变,旧 URL 的内容不会被覆盖,因此可以设置长 max-ageimmutable。HTML 负责引用新 URL,所以 HTML 自身不能用同样的永久缓存策略。

Q5: HTTP/2 下请求数量还重要吗?

答案

仍然重要,但不再是简单的“越少越好”。每个请求仍有 header、调度和服务端成本,过度分包也会增加模块执行;反过来,超大 bundle 又损害缓存和首屏。应围绕关键路径、缓存复用和执行成本选择粒度。

Q6: LCP 图片已经压得很小,为什么 LCP 仍然慢?

答案

可能慢在 TTFB、资源发现、优先级或元素渲染。图片由 JS 晚创建、被懒加载、排在大量 preload 后面,或者下载完成后主线程忙于 Hydration,都可能让 LCP 继续很高。

Q7: async 是否一定比 defer 更快?

答案

不是。async 下载完立即执行,可能在关键渲染阶段打断主线程,且多个脚本顺序不保证;defer 在 DOM 解析后按文档顺序执行,更适合有依赖的应用入口。独立第三方脚本才常使用 async,并仍要治理执行成本。

Q8: Brotli 比 gzip 好,是否所有响应都用最高压缩级别?

答案

静态资源可在构建阶段使用较高压缩级别;动态响应的高压缩级别会增加服务端 CPU 和 TTFB。还要考虑 CDN 是否已压缩、极小文件是否值得压缩,以及客户端兼容回退。

Q9: 如何排查跨域资源 Timing 全是 0?

答案

先检查资源响应是否设置 Timing-Allow-Origin。没有 TAO 时浏览器会隐藏详细阶段和大小,transferSize 为 0 不能直接推断请求失败或命中缓存。

Q10: 为什么 Service Worker 可能导致白屏?

答案

如果 SW 返回旧 HTML 或旧入口脚本,而 CDN 已删除对应 chunk,就会产生版本错配。应按资源类型制定缓存策略、保留旧 hash 资源、正确处理 activate/client claim,并提供受控更新与恢复。

Q11: 前端如何避免接口 waterfall?

答案

无依赖请求同时启动;有依赖请求考虑 BFF 聚合、服务端并行取数、预取或流式返回。还要避免组件层级导致“父组件取完才挂子组件、子组件再取”的隐式 waterfall。

Q12: 如何判断第三方脚本值不值得保留?

答案

记录它的网络体积、主线程时间、长任务和业务收益,通过 request blocking 或灰度实验比较性能与转化。没有负责人、没有收益指标、长期超预算的第三方应删除、替换或只在必要页面按条件加载。

相关链接