图片与字体自托管:少一次外部请求,少一个故障点

引一个 CDN 链接只要一行,但代价是:多一次 DNS 查询、多一次 TLS 握手、多一个可能挂掉或被墙的第三方。自己的东西放在自己手里最稳。

一、第三方 CDN 的隐性成本

每次引用外部资源,浏览器都要走一遍完整流程:

bash
DNS 查询 → TCP 握手 → TLS 握手 → 下载资源

fonts.googleapis.com 这种跨域资源,光握手就是 2~3 个 RTT。在国内网络环境下,这一步经常要几百毫秒甚至超时。

更麻烦的是故障面变大:CDN 挂了、被墙了、改版了,你的站点就跟着出问题,而你什么都做不了。

二、字体自托管

步骤

  1. 下载字体文件(优先 woff2,压缩率最高);
  2. 放到自己的目录;
  3. @font-face 声明;
  4. preload 提前加载。
css
@font-face {
  font-family: "MyFont";
  src: url("/fonts/myfont.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;   /* 关键:字体没加载完先用系统字体显示 */
}

body {
  font-family: "MyFont", -apple-system, "PingFang SC", sans-serif;
}

font-display: swap 很重要:不加的话字体加载期间文字是空白的(FOIT),用户会以为页面坏了;加了会先用后备字体显示,字体到位后再切换(FOUT)。

html
<!-- 关键字体预加载 -->
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>

crossorigin 属性哪怕是同域也必须加,否则字体会被下载两次。

中文站点的现实选择

中文字体动辄 5~10MB,自托管不现实。最实用的方案是不用自定义字体

css
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
             "Hiragino Sans GB", "Microsoft YaHei", sans-serif;

直接用系统内置的苹方、微软雅黑,零请求、零加载、每个平台都好看。本站就是这么做的——最有效的优化是不加载

如果确实需要中文特殊字体,考虑子集化:只保留站点实际用到的字。

bash
# 用 pyftsubset 按实际用到的字符裁剪(几个 MB → 几十 KB)
pip install fonttools brotli
pyftsubset source.ttf --text-file=used-chars.txt --flavor=woff2 --output-file=subset.woff2

三、图标:用 SVG 或图标字体

别引 Font Awesome 全套(几百 KB),只要几个图标的话:

  1. 内联 SVG(最推荐):零请求,还能用 CSS 改颜色;
  2. SVG sprite:多个图标合成一个文件;
  3. 单个 SVG 文件<img src="icon.svg">

本站头部的搜索、主题切换图标就是内联 SVG,直接写在 HTML 里,随页面一起到达。

html
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  <circle cx="11" cy="11" r="7"></circle>
  <path d="M20 20l-3.5-3.5"></path>
</svg>

currentColor 的话,图标颜色会自动跟随父元素的 color,主题切换时不用改两处。

四、图片:格式、尺寸、压缩

格式怎么选

格式适用特点
WebP通用首选比 JPEG 小 30%,现代浏览器全支持
AVIF极致压缩比 WebP 再小 20%,编码慢
JPEG照片兼容性最好
PNG需要透明 / 截图无损,体积大
SVG图标、示意图矢量,任意缩放
html
<!-- 用 picture 做优雅降级 -->
<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="描述" width="800" height="450" loading="lazy">
</picture>

转换与压缩

bash
sudo apt install -y webp imagemagick

# 转 WebP
cwebp -q 80 input.jpg -o output.webp

# 批量转
for f in *.jpg; do cwebp -q 80 "$f" -o "${f%.jpg}.webp"; done

# 压缩 JPEG(原地)
jpegoptim --max=85 *.jpg

# 压缩 PNG
optipng -o2 *.png

尺寸要匹配用途

别用 2000px 的原图当 300px 的缩略图。给出不同尺寸让浏览器挑:

html
<img srcset="img-480.webp 480w, img-800.webp 800w, img-1200.webp 1200w"
     sizes="(max-width: 600px) 480px, 800px"
     src="img-800.webp" alt="" width="800" height="450" loading="lazy">

五、必须写 width / height

图片没有尺寸声明时,加载完成那一刻页面会突然往下顶,用户正要点的按钮被顶走了。这个指标叫 CLS(累积布局偏移),是 Lighthouse 的核心评分项之一。

html
<!-- 好:预留空间,加载时不跳动 -->
<img src="a.webp" width="800" height="450" loading="lazy" alt="">

<!-- 也可以只给比例,让 CSS 决定实际尺寸 -->
img { aspect-ratio: 16 / 9; width: 100%; height: auto; }

六、preload 要克制

preload 是「提前下载」,但不是加得越多越好——它会跟真正重要的资源抢带宽。

只 preload 三类东西:

  1. 首屏关键字体;
  2. 首屏最大的那张图(LCP 元素);
  3. 阻塞渲染的关键 CSS。
html
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.webp" as="image">

其他资源用 prefetch(空闲时下载下一页可能用到的)或干脆不预加载。

七、检查清单

bash
# 找出页面里所有外部域名
curl -s https://www.bzii.cn | grep -oE "https?://[a-z0-9.-]+" | sort -u

# 理想的静态站:外部域名数量 = 0
  • 无第三方 CDN 依赖(JS/CSS/字体/图标)
  • 图片为 WebP/AVIF,尺寸与用途匹配
  • 图片写了 width/height 或 aspect-ratio
  • 非首屏图片有 loading="lazy"
  • 字体用 font-display: swap,或干脆用系统字体
  • preload 只用在真正关键的资源上

一句话原则:能不加载的就不加载,能自己托管的就自己托管。