前端页面性能优化(二)

前端页面性能优化之静态资源与缓存策略

从整个页面的视角来看,影响性能的远不止图片。CSS、JavaScript、字体文件、图标资源、接口响应等,都会直接决定用户能多快看到“可用的页面”和“可交互的页面”。

本文我们重点聚焦在:静态资源与缓存策略


一、静态资源在性能中的角色

从浏览器的视角,静态资源大致可以分为:

  • HTML 文档:通常不缓存或短缓存,承载结构与入口。
  • CSS 样式:决定首屏布局与样式,是关键渲染路径的一部分。
  • JavaScript 脚本:决定交互与业务逻辑,可能会阻塞渲染。
  • 字体文件:影响文字渲染体验,体积也可能不小。
  • 图片与图标:上篇已经重点讲过,这里只作为整体的一部分。

用户能否快速看到首屏,核心在于:

  • 关键 CSS / JS 能否尽快拿到
  • 非关键资源能否延后 / 按需加载
  • 重复访问时能否直接命中缓存

这就引出了本文的主角:缓存策略 + 静态资源管理


二、HTTP 缓存基础:强缓存与协商缓存

浏览器缓存机制大致分为两类:

  • 强缓存(Freshness / Expiration):在有效期内,直接从本地缓存读取,不发请求。
  • 协商缓存(Validation):向服务器发一个轻量请求,询问资源是否有更新。

理解这两者,是设计缓存策略的基础。

1. 强缓存:Cache-Control / Expires

典型配置:

1
Cache-Control: max-age=31536000, public
  • max-age=31536000:资源在 1 年内视为“新鲜”,浏览器可直接使用。
  • public:允许中间代理(比如 CDN)缓存。

老一点的写法是 Expires

1
Expires: Wed, 21 Oct 2026 07:28:00 GMT

但在现代实践中,推荐以 Cache-Control 为主

2. 协商缓存:ETag / Last-Modified

当资源不适合长期强缓存(比如接口返回、频繁变动的文件)时,可以使用协商缓存。

  • Last-Modified / If-Modified-Since

    • 服务器在响应头里带上 Last-Modified
    • 浏览器下次请求时带上 If-Modified-Since
    • 若资源未变化,服务器返回 304 Not Modified,不再重复传输内容。
  • ETag / If-None-Match

    • 服务器为资源生成一个唯一标识(可以是文件指纹或哈希)。
    • 浏览器下次请求时通过 If-None-Match 携带。
    • 服务器对比后返回 304 或 200。

实践建议

  • 静态构建出的资源(带指纹的 .js.css 等)→ 优先用强缓存
  • 动态资源、接口返回 → 合理使用协商缓存,避免重复传输。

三、文件指纹与“缓存友好”的发布策略

几乎所有现代前端工程化方案(Webpack / Vite / Rollup 等),都会默认为构建产物添加“文件指纹”:

1
2
3
app.23fd8a9c.js
vendor.9b0b71c2.js
index.41d2e8e1.css

1. 为什么需要文件指纹?

我们希望浏览器对静态资源长期强缓存,但又要保证代码更新时用户能拿到新版本。文件指纹正是用来解决这个矛盾:

  • 内容不变 → 文件名不变 → 强缓存命中。
  • 内容变更 → 指纹变化 → 文件名变化 → 浏览器视为新资源重新拉取。

因此一个经典的实践是:

1
Cache-Control: max-age=31536000, immutable

immutable 告诉浏览器:在 max-age 期间,这个文件不会变,不必发送条件请求,进一步减少开销。

2. 如何让 HTML 始终拉到“最新资源”?

通常发布策略是:

  • HTML 不带强缓存或设置较短的 max-age
  • HTML 中引用的 JS / CSS 带有指纹,并被设置为一年以上的强缓存。

例如:

1
2
3
4
5
# 对 HTML
Cache-Control: max-age=60, no-cache

# 对静态资源(含指纹)
Cache-Control: max-age=31536000, immutable

解释:

  • HTML 的 no-cache 表示每次使用前需要向服务器确认是否有新版本(可能返回 304,也可能返回 200)。
  • 静态资源一旦下发,就可以放心长期缓存。

3. SPA / 多页应用中的差异

  • SPA

    • HTML 通常只有一个入口。
    • 强烈建议设置短缓存 + no-cache,以确保路由和入口脚本引用更新。
  • 多页应用

    • 可以按路由拆分 HTML。
    • 常见做法依然是“HTML 短缓存 + 资源长缓存”。

四、CDN 与就近访问:把资源“搬到用户旁边”

在图片优化那一篇中已经提到,CDN 对于图片非常重要。对 JS / CSS / 字体 / 接口等来说,同样如此。

1. 为什么要用 CDN?

  • 物理距离更近:减少 RTT(往返时间)。
  • 多节点分担流量:缓解源站压力,提升并发能力。
  • 协议支持更好:很多 CDN 默认支持 HTTP/2 / HTTP/3、TLS 优化等。

2. 常见的静态资源部署结构

  • 前端静态资源托管到 CDN 域名:
    • https://static.xxx.com/app.23fd8a9c.js
    • https://static.xxx.com/index.41d2e8e1.css
  • 源站一般是对象存储或静态服务器:
    • 如 OSS / COS / S3 / Nginx 等。

3. CDN 配置中的关键点

  • 开启缓存与压缩

    • 启用 Gzip / Brotli 压缩。
    • 遵循源站的 Cache-Control,或在 CDN 配规则覆盖。
  • 正确处理回源与版本

    • 使用带指纹的路径,避免缓存“脏数据”。
    • 更新版本时,可以只上传新文件,由于文件名变化,CDN 会自动回源。
  • 利用 CDN 的“格式自适应”能力

    • 类似上一篇讲的 ?format=auto,对于图片尤为常见。
    • 有些 CDN 对静态文本资源也提供自动压缩、合并等优化能力(视产品而定)。

五、预加载 / 预获取:让关键资源更早到达

缓存解决的是“减少重复下载”,而预加载 / 预获取则是“把本来会晚一点下载的东西,提前一点拿到”。

1. preload:提前加载当前页面“必需”资源

<link rel="preload"> 可以让浏览器在更早的阶段加载特定资源,例如:

1
2
<link rel="preload" href="/static/app.23fd8a9c.js" as="script" />
<link rel="preload" href="/static/index.41d2e8e1.css" as="style" />

使用要点:

  • 只对关键且确定会使用的资源使用。
  • 正确设置 as 属性,有利于优先级调度和安全策略。

2. prefetch:为“下一步的页面”提前准备资源

<link rel="prefetch"> 更适合未来可能用到的资源,比如下一页的 JS 包:

1
<link rel="prefetch" href="/static/page-about.1a2b3c4d.js" as="script" />

浏览器会在空闲带宽时下载这类资源,不会阻塞当前页面的关键加载。

典型场景:

  • 单页应用中,对“高概率访问”的路由提前 prefetch。
  • 用户停留在首页较久时,预取下一步引导页的资源。

3. dns-prefetch / preconnect:减少网络握手开销

如果你使用了多个域名(如 API 域名、CDN 域名、第三方 SDK 域名等),可以通过:

1
2
3
4
5
<!-- 提前做 DNS 解析 -->
<link rel="dns-prefetch" href="//static.xxx.com" />

<!-- 提前建立 TCP / TLS 连接 -->
<link rel="preconnect" href="https://static.xxx.com" crossorigin />

适用场景:

  • 页面上一定会访问的外部域名,且连接本身耗时明显。

六、字体资源优化:看得见的“加载闪烁”

字体文件(ttf / woff / woff2 等)在现代页面中越来越常见,尤其是定制化 UI、图标字体等。但字体加载不好,很容易出现:

  • 白板无字(FOIT:Flash of Invisible Text)
  • 字体闪烁(FOUT:Flash of Unstyled Text)

1. 使用 font-display 控制渲染策略

@font-face 中增加 font-display,例如:

1
2
3
4
5
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: swap;
}

常用取值:

  • swap:优先用系统字体渲染,字体加载完成后再切换。
  • fallback:如果在一小段时间内未加载完成,就一直用系统字体。

这两种策略可以有效避免长时间“无字可见”的情况。

2. 字体子集化:只保留需要的字符

很多时候我们只需要一小部分字符(例如中文 + 英文 + 数字 + 常用标点),可以通过子集化工具裁剪字体集:

  • 例如 fonttoolspyftsubset、在线子集化服务等。
  • 结合工程构建,在 CI 流程中自动生成子集字体。

子集化后:

  • 文件体积显著减小。
  • 加载时间缩短,首次渲染更快。

3. 优先使用系统字体 / 本地字体

对性能要求较高的业务(如后台管理、工具类产品),完全可以:

  • 优先使用系统字体,避免额外字体请求。
  • 或通过 local() 优先命中本地已有字体。

示例:

1
2
3
4
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}

七、静态资源压缩与打包:更小、更少、更快

在图片格式优化的基础上,其他静态资源同样需要压缩与按需加载。

1. 文本压缩:Gzip / Brotli

确保服务端或 CDN 开启对以下类型资源的压缩:

  • text/html
  • text/css
  • application/javascript
  • application/json
  • image/svg+xml

现代 CDN/服务端一般都支持 Gzip 和 Brotli:

  • Brotli 在大多数场景下压缩率更优,推荐优先启用。
  • 浏览器会根据 Accept-Encoding 决定使用哪种压缩。

2. JS / CSS 按需拆分与 Tree Shaking

构建时应尽量做到:

  • 只打包实际用到的代码(Tree Shaking)。
  • 根据路由 / 功能模块进行代码拆分(Code Splitting)。
  • 公共依赖抽离成 vendor 包,便于缓存。

示例(以 Webpack 为例,简化说明):

1
2
3
// 按路由拆分
const About = () => import("./pages/About.vue");
const Home = () => import("./pages/Home.vue");

配合合理的命名策略,可以在构建时生成:

  • home.aa11bb22.js
  • about.3344cc55.js

配合缓存策略,实现“页面拆分 + 长缓存”。


八、实战中的资源与缓存策略搭配方案

综合上面的内容,可以给出一套比较实用的“默认方案”(可根据业务再细化调整)。

1. 静态构建产物

  • 文件命名:带内容指纹,如 app.[hash].jsindex.[hash].css
  • HTTP 头:
1
Cache-Control: max-age=31536000, immutable

2. HTML 文档

  • 不加指纹,路径稳定(如 /index.html)。
  • HTTP 头:
1
Cache-Control: max-age=60, no-cache

3. 图片资源

  • 根据上一篇的建议,采用 WebP / AVIF + JPG/PNG 兜底。
  • 静态图片同样使用指纹 + 长缓存。

4. 字体资源

  • 尽量做子集化。
  • 采用 font-display: swapfallback
  • 静态字体文件可以使用长缓存。

5. 接口 / 动态资源

  • 对 GET 请求使用合理的协商缓存(ETag / Last-Modified)。
  • 对 POST、灵活接口,结合业务决定是否缓存或禁用缓存。

九、总结

在前端性能优化的体系中,静态资源与缓存策略是和“图片优化”同等重要的基础能力:

  • 通过 文件指纹 + 长缓存,让浏览器把不会变的资源牢牢记住。
  • 通过 HTML 短缓存 + 缓存验证,确保更新能及时到达用户。
  • 通过 CDN + 压缩 + 预加载 / 预获取,让关键资源更快、更近地送达。
  • 通过 字体与文本资源优化,减少“肉眼可见”的加载闪烁与卡顿。

只有把这些基础做好了,用户才能在第一时间看到一个“可用的页面”,为后续的交互流畅打下坚实的基础。


前端页面性能优化(二)
https://sunjc.vip/2025/08/05/前端页面性能优化(二)/
作者
Sunjc
发布于
2025年8月5日
许可协议