网页字体实战

中文网页字体从加载原理到上线:拆子集、按字分片、写对 @font-face、加载策略、减少跳动、字体栈、自托管许可、排查和新技术。

浏览器怎么加载网页字体

适合谁看:想搞清楚网页字体从请求到显示经历了什么、为什么会「白屏」或「跳一下」的前端开发者。

@font-face 的基本写法见《在网页里使用字体:@font-face 的现代写法》。本页讲背后的过程,后面几页的优化都从这里出发。

一次加载的顺序

  1. 下载 HTML,发现 CSS。 字体 CSS 写在 <link> 里会较早开始下载;写在 @import 里要等外层 CSS 下载完才开始。
  2. 解析 CSS,登记 @font-face。 这一步只是登记「有这么一个字体、文件在哪里」,不会下载字体文件。
  3. 排版时发现要用它。 页面上某段文字的 font-family 用到了这个字体,并且文字里的字符落在它的 unicode-range 里,浏览器才开始下载对应的文件。
  4. 等待期间先用别的字体。 字体没到时,按 font-display 的规则,要么先不显示文字,要么先用备用字体显示。
  5. 字体到了,重新排版。 换成新字体后,字宽、行高不同,段落会重新折行,这就是「跳一下」。

所以字体文件开始下载的时间,比 HTML、CSS 都晚,常常排在关键请求的最后面。

「白屏」和「跳一下」

现象 常见叫法 原因
文字区域空白一段时间 FOIT(隐形文字闪烁) 浏览器在等字体,期间文字不画出来
先显示系统字体,然后换掉 FOUT(无样式文字闪烁) 先用备用字体,字体到了再替换

CSS 规范对 font-display 的建议是:block 最多等约 3 秒;swap、fallback、optional 只等很短的时间(建议不超过 100 毫秒)就先显示备用字体。中文页面字体大,block 会让读者盯着空白很久,所以一般用 swap,见《加载策略:font-display、预连接和预加载》。

跨域字体要允许跨域

字体文件放在另一个域名时,服务器必须返回 Access-Control-Allow-Origin 响应头,否则浏览器会拒绝使用。自己部署字体时要记得配置。

中文为什么特别

一款中文字体通常有几万个字形,单个文件常有几 MB 到十几 MB。直接引用整个文件,字体要很久才能显示。解决办法就是把它拆成很多小文件,每个文件用 unicode-range 标明包含哪些字,浏览器只下载用到的那几片。原理见《中文网页字体为什么要拆分子集》,动手做见《动手拆子集:pyftsubset》 和《按字分片:cn-font-split》。

参考资料

动手拆子集:pyftsubset

适合谁看:想把一款字体裁成只含页面用到的字、转成 WOFF2 自己部署的开发者。

先确认许可允许修改和网页使用,见《自托管网页字体的许可要点》。

准备

pyftsubset 是 fontTools 自带的子集工具。要输出 WOFF2,还需要 brotli:

pip install fonttools brotli

最常用的三种用法

按一段文字裁: 适合标题、Logo 文字、活动页。

pyftsubset SourceHanSerifSC-Bold.otf \
  --text="春日限定 立即报名" \
  --flavor=woff2 \
  --output-file=title.woff2

按文字文件裁: 把页面上所有文字导出到一个文本文件。

pyftsubset font.ttf --text-file=chars.txt --flavor=woff2 --output-file=page.woff2

按码位范围裁: 例如只留基本拉丁字母和中日韩统一表意文字基本区。

pyftsubset font.ttf --unicodes="U+0020-007E,U+4E00-9FFF" --flavor=woff2 --output-file=cjk-basic.woff2

别把许可信息裁掉

pyftsubset 默认只保留字体名称表里编号 0 到 6 的记录,并且只保留英文的记录。许可说明(编号 13)和许可网址(编号 14)默认会被删掉,中文字体名也会被删掉。自己部署开源字体时,建议加上:

--name-IDs='*' --name-languages='*'

这样字体文件里仍然带着版权和许可信息。

排版功能

pyftsubset 默认保留一批常用的 OpenType 功能,包括竖排相关的 vert、vrt2 和标点挤压相关的 halt、vhal、palt、vpal、chws、vchw 等。要指定保留哪些功能,用 --layout-features;要保留全部,用 --layout-features='*'。

注意事项

参考资料

按字分片:cn-font-split

适合谁看:内容不固定(博客、社区、电商),想把整款中文字体拆成很多小片、按需加载的开发者。

分片和子集的区别

文风网页字体用的就是分片,见《性能:子集、字体显示策略和预连接》。自己部署时,可以用开源工具 cn-font-split 生成同样的结构。

cn-font-split 简介

cn-font-split 是「中文网字计划」使用的分片工具,Apache-2.0 许可。它会:

用法示例(Node.js)

npm install cn-font-split
import fs from 'fs';
import { fontSplit } from 'cn-font-split';

const input = new Uint8Array(fs.readFileSync('./LXGWWenKai-Regular.ttf').buffer);
await fontSplit({
  input,
  outDir: './dist/font',
  css: { fontFamily: 'My WenKai', fontWeight: '400' },
});

参数以官方文档为准,版本更新时可能变化。它还提供命令行、在线切割页面和前端构建插件。

分片后怎么用

  1. 把输出目录整个放到网站上(或对象存储),保证跨域头和缓存头正确;
  2. 在页面里引入生成的 CSS;
  3. 在样式里写 CSS 里声明的 font-family 名字和备用字体。

多个字重要分别切,每个字重一份 CSS。这几份 CSS 的 font-family 用同一个名字,font-weight 各不相同,写法见《写对 @font-face:字体名、字重和 local()》。

分片的代价

参考资料

写对 @font-face:字体名、字重和 local()

适合谁看:自己写 @font-face、或自己部署多字重字体的开发者。

@font-face 写错了,浏览器多半不会报错,只会悄悄用错字体、自己加粗,或者多下载文件。下面是几条容易忽略的正确写法。

一个家族,一个名字

同一款字的不同字重,font-family 应该写同一个名字,用 font-weight 区分:

@font-face {
  font-family: "My Sans";
  src: url("my-sans-regular.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "My Sans";
  src: url("my-sans-bold.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}

这样页面里写 font-family: "My Sans",正文自动用 400,<strong>、<h1> 自动用 700。

如果每个字重起不同的名字(如 "My Sans Regular"、"My Sans Bold"),浏览器会把它们当成两款字。给 <strong> 加粗时,它找不到同名的 700,就会把 400 人工加粗,效果很差。

字重数值要和文件对应

font-weight 要写这个文件实际的粗细。常见对应关系:

数值 常见名称
100 Thin、Hairline
200 ExtraLight、UltraLight
300 Light
400 Regular、Normal
500 Medium
600 SemiBold、DemiBold
700 Bold
800 ExtraBold、UltraBold
900 Black、Heavy

字体文件 OS/2 表里的 usWeightClass 记录了厂商给的数值,可以用它来对照。

两点要注意:

可变字体写一个范围

可变字体一个文件包含连续的字重,用两个值写范围:

@font-face {
  font-family: "My Sans VF";
  src: url("my-sans-vf.woff2") format("woff2");
  font-weight: 100 900;
}

local() 的正确写法

local() 让浏览器先找用户电脑上装好的同名字体,找到了就不下载。写法要点:

src: local("Source Han Sans SC Regular"),
     local("SourceHanSansSC-Regular"),
     url("source-han-sans-sc-regular.woff2") format("woff2");

什么时候不要用 local():

拿不准就不写 local(),只写 url()。

记得 format()

format("woff2") 告诉浏览器文件格式,不支持的格式会直接跳过,不会白白下载。今天所有主流浏览器都支持 WOFF2,一般只提供 WOFF2 就够了。

参考资料

加载策略:font-display、预连接和预加载

适合谁看:已经能用上网页字体,想让首屏更快、闪动更少的前端开发者。

font-display 怎么选

取值 等待字体(期间不显示文字) 换成新字体的时机 适合
auto 由浏览器决定,常见是 block — 不建议依赖
block 较长,规范建议约 3 秒 字体到了就换 图标字体等不能用别的字体代替的场合
swap 极短 字体到了就换,不限时间 中文正文、标题的默认选择
fallback 极短 约 3 秒内到了才换,晚了这次就不换 正文,希望减少晚到的跳动
optional 极短 基本只在字体已缓存时使用 字体可有可无、最在意稳定的页面

中文字体体积大、首访很难在 100 毫秒内到达,optional 的结果常常是首访看不到字体。一般选 swap,再用《减少跳动:后备字体和尺寸调整》 里的方法降低跳动。

预连接

字体放在另一个域名时,在 <head> 前部加:

浏览器会提前完成 DNS、TCP、TLS 握手。字体请求是匿名跨域请求,所以要带 crossorigin。

预加载:只用在确定的那一个文件上

用 JavaScript 控制(可选)

字体加载 API 可以检查和等待字体:

// 等页面上所有字体加载完成
await document.fonts.ready;
// 主动加载某个字重
await document.fonts.load('700 1em "My Sans"');
// 检查是否已可用
document.fonts.check('16px "My Sans"');

适合「字体到了再执行动画」「截图前确保字体已加载」这类场景。普通页面用 CSS 就够了。

缓存

字体文件很少变化,建议给它们设置长期缓存(例如一年),文件内容变了就换文件名。

参考资料

减少跳动:后备字体和尺寸调整

适合谁看:用了 font-display: swap,但字体换上来时页面「跳一下」,想减少布局偏移(CLS)的开发者。

为什么会跳

备用字体和网页字体的字宽、上下留白不同。换字体时,一行能放下的字数变了,行高也可能变了,后面的内容就整体移动。

对中文来说:

第一步:写死行高

给正文写一个明确的行高,行高就不再依赖字体:

body { line-height: 1.75; }

这是最简单、效果最明显的一步。排版上的行高建议见《正文用字:字号、行距和字重》。

第二步:选接近的备用字体

备用字体和网页字体越接近,跳动越小:

第三步:用尺寸调整描述符微调

可以为备用字体单独写一条 @font-face,用描述符把它「调」得更像网页字体:

@font-face {
  font-family: "My Sans Fallback";
  src: local("PingFang SC Regular"), local("Microsoft YaHei");
  size-adjust: 100%;
  ascent-override: 88%;
  descent-override: 12%;
  line-gap-override: 0%;
}
body { font-family: "My Sans", "My Sans Fallback", sans-serif; }
描述符 调整什么
size-adjust 整体缩放字形
ascent-override 基线以上的高度
descent-override 基线以下的高度
line-gap-override 行间额外空隙

上例中的百分比只是示意,要按两款字的实际度量计算。

浏览器支持情况(据 MDN 兼容性数据):size-adjust 在 Chrome 92、Firefox 92、Safari 17 起支持;ascent-override 等三项在 Chrome 87、Firefox 89 起支持,Safari 正式版尚未支持。不支持的浏览器会忽略这些描述符,不会出错。

怎么量

参考资料

中文字体栈怎么写

适合谁看:要给中文网站写 font-family 的前端开发者,不管有没有用网页字体。

字体栈是什么

font-family 可以写一串字体名,浏览器从左到右找,逐个字符判断:第一个字体里没有这个字,就用下一个字体显示这个字。所以字体栈既是「首选不在时的备选」,也是「首选缺字时的补字」。

常见系统中文字体

系统 黑体类 宋体类 楷体类
macOS、iOS PingFang SC(苹方) Songti SC(宋体-简) Kaiti SC(楷体-简)
Windows Microsoft YaHei(微软雅黑) SimSun(宋体) KaiTi(楷体)
Android 系统自带,通常是 Noto Sans CJK / 思源黑体,部分厂商用自家字体 视机型而定 视机型而定
Linux 桌面 Noto Sans CJK SC、Source Han Sans SC、WenQuanYi Micro Hei 等 Noto Serif CJK SC 等 视发行版而定

不同系统版本、不同厂商的预装字体有差别,上表只是常见情况。macOS 上的部分字体需要在「字体册」里下载后才能用。

推荐写法

黑体类:

font-family: "网页字体名", -apple-system, "PingFang SC", "Hiragino Sans GB",
  "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", sans-serif;

宋体类:

font-family: "网页字体名", "Songti SC", "Noto Serif CJK SC", "Source Han Serif SC",
  "SimSun", serif;

几个要点

缺字的情况

生僻字、新收录的扩展区汉字,系统字体也可能没有。要保证这些字能显示,可以在字体栈末尾加一款覆盖范围大的字体,见《网页和系统里怎么显示生僻字》。

参考资料

用文风网页字体:一页看懂正确接法

适合谁看:准备在网站上用文风网页字体,想一次把关键做法看全的开发者和站长。

本页是要点汇总,细节在《文风开发者文档》各页。

地址

https://cn.windfonts.com/fonts-packages/{目录名}/{字重}/{子集}/result.css

详见《网页字体地址规则》。

推荐的引入代码

<!-- 本页嵌入的字体由文风字体(Windfonts)提供,可自由使用;请保留本授权标注 https://wenfeng.org/license -->
<link rel="preconnect" href="https://cn.windfonts.com" crossorigin>
<link rel="stylesheet" href="https://cn.windfonts.com/fonts-packages/xwwk/Regular/full/result.css">

字体名:从 CSS 里复制

不同字体 CSS 里声明的 font-family 名字格式不一样。在浏览器里打开 result.css,搜 font-family,复制引号里的名字,不要按目录名或字体中文名去猜。

body {
  font-family: "从 CSS 里复制的名字", "PingFang SC", "Microsoft YaHei", sans-serif;
  line-height: 1.75;
}

多字重:先看 CSS 里的数值

每个字重一份 CSS。引入多份之前,打开每份 CSS 看 font-weight 的数值:

详见《在 CSS 里使用:字体名、备用字体和多字重》。

已经帮你做好的事

不需要、也不建议做的事

先看许可

文风收录的字体许可各不相同。上线前打开字体页的许可原文,确认网页使用在许可范围内,见《这款字能不能商用:五步自查》。

参考资料

自托管网页字体的许可要点

适合谁看:打算把字体文件放在自己服务器上做网页字体的开发者、站长和企业法务。

网页字体是一种「分发」

网页字体的原理是让访客的浏览器下载字体文件。从许可的角度看,你在把字体文件提供给每一位访客。所以:

OFL 字体自托管的四个要点

  1. 保留版权和许可信息。 字体文件内部的版权、许可字段不要删;拆子集时加上保留名称表的选项,见《动手拆子集:pyftsubset》。
  2. 转 WOFF2 不一定算修改,拆子集一定算修改。 只做 WOFF2 压缩、数据和元数据完整保留,可以不算修改(OFL-FAQ 2.2);拆子集、删掉排版功能都是修改(OFL-FAQ 2.6)。
  3. 修改版不能用保留字体名。 字体声明了保留名的,拆过子集的版本要改名,除非能做到「功能等价」或者和作者另有约定(OFL-FAQ 2.7、2.8)。
  4. 网站条款不能收紧 OFL。 你可以对字体文件做防盗链,但不能宣称别人不能按 OFL 使用这款字(OFL-FAQ 1.17、1.24)。

逐条解读见《SIL 开放字体许可证(OFL)逐条读》。

改名怎么改

改的是字体文件内部的名字(名称表里的家族名、完整名、PostScript 名),以及你 CSS 里 font-family 的名字。可以用 fontTools 修改,见《fontTools 入门:查看、修改、转格式》。新名字不要包含原字体的保留名。

页面上要不要署名

OFL 不强制在网页上署名(OFL-FAQ 1.1.2)。但在网站的「关于」或页脚写明所用字体和许可,对访客和作者都友好。

使用文风的切片包

文风提供部分字体的切片包,许可仍然以每款字的许可原文为准,切片包里的授权注释请保留,见《自托管切片与 WordPress 主题》。

参考资料

字体没生效?一套排查顺序

适合谁看:网页字体「好像没用上」「只有部分字变了」「变粗了」,想自己查原因的开发者。

文风网页字体的专门问答见《开发常见问题》。本页是通用的排查顺序,自托管、第三方字体也适用。

1. 看渲染字体

Chrome 或 Edge:开发者工具 →「元素」→ 选中文字 →「计算样式」面板拉到底,看「渲染字体(Rendered Fonts)」。它列出这段文字实际用了哪些字体、各显示了多少个字。

Firefox:开发者工具 →「检查器」→「字体」面板,可以看到页面用到的字体和来源。

2. 看网络请求

「网络」面板筛选「字体」和「CSS」,刷新:

看到的情况 可能原因
没有任何字体请求 font-family 名字和 @font-face 不一致;或页面上没有文字用到它
404 地址拼错,大小写不对,或子集不存在
请求被拦截,控制台有 CORS 报错 字体服务器没有返回允许跨域的响应头
200,但文件很小或类型不对 服务器返回了错误页面,打开地址看看内容

3. 核对名字

font-family 里写的名字必须和 @font-face 里声明的完全一致(大小写、空格、连字符)。用第三方 CSS 时,打开 CSS 复制名字,不要凭印象写。

4. 部分字是别的字体

5. 字被人工加粗或拉斜

6. 用控制台确认

在开发者工具的控制台里运行:

document.fonts.check('16px "字体名"')   // true 表示已可用
[...document.fonts].map(f => [f.family, f.weight, f.status])

7. 清缓存再试

字体更新后,浏览器和服务器缓存可能还是旧文件。用无痕窗口或「停用缓存」再试一次。

参考资料

可变字体和增量字体传输

适合谁看:关注网页字体新技术、想知道哪些现在能用、哪些还要等的开发者。

可变字体:现在就能用

可变字体(Variable Font)把一个家族的多个字重(以及宽度、倾斜等)放进一个文件,用「轴」连续调节。

中文可变字体的取舍:

字重的设计知识见《多字重家族和可变字体怎么挑》。

增量字体传输(IFT):还在路上

增量字体传输(Incremental Font Transfer,IFT)是 W3C 正在制定的标准。它的思路是:浏览器先下载字体的一小部分,页面需要更多字符时,再下载「补丁」扩展同一个字体文件。

和今天的 unicode-range 分片相比,它的好处是:

现状(截至 2026 年 10 月):W3C 已发布候选推荐草案(2026 年 10 月 13 日版);Chrome 已表示有意开始原型开发,主流浏览器尚未正式支持。现阶段中文网页字体仍然用 unicode-range 分片。

OFL 常见问题也专门讨论了 IFT:用 IFT 这类按需子集技术提供 OFL 字体是可以的,前提是达到足够的「功能等价」,不能破坏字符覆盖和排版功能(OFL-FAQ 2.9)。

参考资料