---
title: "自托管字体的一年"
description: "从 Google Fonts 换成自托管，从自托管换成子集化分片。聊聊为什么一个中文字体能从 25MB 变成实际加载 180KB。"
canonical_url: "https://www.kemiaosw.top/posts/self-hosted-fonts"
---

> Index of every page on this site: [llms.txt](https://www.kemiaosw.top/llms.txt)

中文网站做自托管字体，最大的障碍不是版权，是**体积**。

一套完整的霞鹜文楷，woff2 格式大约 20-25 MB。用户打开首页就要下载这个？显然不行。

所以大部分中文站的选择是：用系统字体。然后全站看起来就像一张没洗过的 PPT。

## 分片是怎么解决的

关键洞察：**一篇文章不可能用到所有汉字。**

常用的 3500 个汉字覆盖了 99.5% 的日常文本。剩下几万个字可能一辈子都不会出现在我的博客里。

所以现代中文字体包的做法是：

1. 按 Unicode 范围把字体切成几百个小文件
2. 每个文件用 `unicode-range` 声明自己负责哪些码位
3. 浏览器只下载**页面实际用到的**那些分片

```css
/* 分片 #12 负责这些码位 */
@font-face {
  font-family: 'LXGW WenKai Lite';
  font-weight: 400;
  font-display: swap;
  src: url('./files/subset-12.woff2') format('woff2');
  unicode-range: U+4E00-4E1F, U+4E30-4E4F;
}
```

浏览器渲染一段文字时，会先检查每个字符属于哪个 `unicode-range`，只下载覆盖到的分片。

## 实际数字

我这个博客用 `lxgw-wenkai-lite-webfont`：

<table>
<thead>
  <tr>
    <th>
      项目
    </th>
    
    <th>
      数值
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      包总大小
    </td>
    
    <td>
      25.1 MB
    </td>
  </tr>
  
  <tr>
    <td>
      分片数量
    </td>
    
    <td>
      582 个
    </td>
  </tr>
  
  <tr>
    <td>
      单篇长文实际加载
    </td>
    
    <td>
      3-5 个分片
    </td>
  </tr>
  
  <tr>
    <td>
      实际传输
    </td>
    
    <td>
      <strong>
        约 180 KB
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      首屏延迟
    </td>
    
    <td>
      0（<code>
        font-display: swap
      </code>
      
      ）
    </td>
  </tr>
</tbody>
</table>

25MB 的包，实际只用了 180KB。

## font-display: swap 的必要性

```css
@font-face {
  font-display: swap;
}
```

`swap` 的意思是：**先用后备字体渲染，字体加载完再换。**

代价是会看到一次字体闪烁（FOIT → FOUT）。收益是**文字永远可读**。

另外两个选项都不适合中文：

- `block`：字体没加载完就不显示文字。中文 180KB 可能要 500ms，用户会看到一片空白
- `optional`：网络慢就永远不用自定义字体。会导致同一篇文章有时是文楷有时是黑体

`swap` 是唯一合理的选项。

## 后备字体链要写对

```css
font-family:
  'Inter Variable',
  'LXGW WenKai Lite',
  'PingFang SC',
  'Microsoft YaHei',
  sans-serif;
```

这个链的顺序有讲究：

1. **Inter** 负责拉丁字母和数字 —— 它的西文比文楷好看得多
2. **文楷** 负责汉字 —— 在 Inter 没有对应字形时接管
3. **苹方 / 微软雅黑** —— 文楷分片还没加载完时的后备
4. **sans-serif** —— 最后兜底

关键在于第 1 和第 2 的顺序：浏览器是逐字符查找的。英文字符在 Inter 里能找到，就不会去问文楷；汉字在 Inter 里找不到，自然落到文楷。

这样西文和中文各用各的最优字体，不需要任何 `unicode-range` 技巧。

## 等宽字体只给数据

```css
font-family: 'JetBrains Mono', 'LXGW WenKai Lite', ui-monospace, monospace;
```

等宽字体我用在四个地方，只有四个：

- 日期（`2025-10-28`）
- 阅读时长（`6 min`）
- 代码块
- `kbd` 元素

**不用它做装饰性的小标签。** 那些 `MONOSPACE` 风格的 eyebrow 文本是模板化的标志之一——它看起来"有技术感"，但没传达任何信息。

## 一个坑

字体分片依赖 `unicode-range`，而 `unicode-range` 是**静态声明**的。

意思是：如果你的文章里出现了某个生僻字，而那个字所在的分片没有被任何 `@font-face` 覆盖（有些字体包会漏掉扩展区），那个字就会掉到后备字体。

结果是一篇文章里 99% 是文楷，中间蹦出一个黑体的字。

我的处理是接受它——比为了一个字加载一整个 3MB 的扩展区划算得多。

## 结论

> 中文字体不是"能不能自托管"的问题，是"有没有做分片"的问题。

用了分片之后：

- 首屏字体成本 ≈ 180KB（可接受）
- 视觉一致性 100%（全站统一字体）
- 零第三方请求（隐私友好，也不受 CDN 挂掉影响）

这套方案我已经跑了一年，没有换回去的理由。

## Sitemap

See the full [sitemap](https://www.kemiaosw.top/sitemap.md) for all pages.
