网页字体实战 中文网页字体从加载原理到上线:拆子集、按字分片、写对 @font-face、加载策略、减少跳动、字体栈、自托管许可、排查和新技术。 浏览器怎么加载网页字体 适合谁看:想搞清楚网页字体从请求到显示经历了什么、为什么会「白屏」或「跳一下」的前端开发者。 @font-face 的基本写法见《在网页里使用字体:@font-face 的现代写法》。本页讲背后的过程,后面几页的优化都从这里出发。 一次加载的顺序 下载 HTML,发现 CSS。 字体 CSS 写在 里会较早开始下载;写在 @import 里要等外层 CSS 下载完才开始。 解析 CSS,登记 @font-face。 这一步只是登记「有这么一个字体、文件在哪里」,不会下载字体文件。 排版时发现要用它。 页面上某段文字的 font-family 用到了这个字体,并且文字里的字符落在它的 unicode-range 里,浏览器才开始下载对应的文件。 等待期间先用别的字体。 字体没到时,按 font-display 的规则,要么先不显示文字,要么先用备用字体显示。 字体到了,重新排版。 换成新字体后,字宽、行高不同,段落会重新折行,这就是「跳一下」。 所以字体文件开始下载的时间,比 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》。 参考资料4@font-face字体最佳实践CSS Fonts Module Level 4 动手拆子集: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='*'。 注意事项 按页面文字裁,内容一变就缺字。 有用户输入、评论、搜索的地方,不要用固定子集,用分片方案(见下一页)。 记得标点和数字。 中文标点、全角空格、数字、常用符号都要包含在内。 改名。 开源字体有「保留字体名」时,裁过的字体按许可算修改版, font-family 和文件里的名字最好换掉,见《SIL 开放字体许可证(OFL)逐条读》。 参考资料3subset: Generate subsets of fonts or optimize file sizesfontTools GitHub 仓库OpenType 规范:name 表 按字分片:cn-font-split 适合谁看:内容不固定(博客、社区、电商),想把整款中文字体拆成很多小片、按需加载的开发者。 分片和子集的区别 子集:只留下一部分字,其他字不要了。 分片:一个字也不丢,把整款字体切成很多片,每片几十 KB,用 unicode-range 标明范围。页面用到哪些字,浏览器就只下载那几片。 文风网页字体用的就是分片,见《性能:子集、字体显示策略和预连接》。自己部署时,可以用开源工具 cn-font-split 生成同样的结构。 cn-font-split 简介 cn-font-split 是「中文网字计划」使用的分片工具,Apache-2.0 许可。它会: 按字符使用频率和字体内容分组,生成一批 WOFF2 分片; 生成一份 CSS,里面是多条 @font-face,每条带 unicode-range; 可选生成预览图和测试页面。 用法示例(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' }, }); 参数以官方文档为准,版本更新时可能变化。它还提供命令行、在线切割页面和前端构建插件。 分片后怎么用 把输出目录整个放到网站上(或对象存储),保证跨域头和缓存头正确; 在页面里引入生成的 CSS; 在样式里写 CSS 里声明的 font-family 名字和备用字体。 多个字重要分别切,每个字重一份 CSS。这几份 CSS 的 font-family 用同一个名字, font-weight 各不相同,写法见《写对 @font-face:字体名、字重和 local()》。 分片的代价 请求数变多。HTTP/2 和 HTTP/3 下影响不大; 同一个词的几个字可能落在不同分片里,先后到达时会看到字逐个替换; 跨分片的连字、上下文替换可能失效,这对中文影响较小,对西文连字、阿拉伯文等影响较大。 参考资料3cn-font-split GitHub 仓库中文网字计划网站unicode-range 写对 @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, 、

自动用 700。 如果每个字重起不同的名字(如 "My Sans Regular"、 "My Sans Bold"),浏览器会把它们当成两款字。给 加粗时,它找不到同名的 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 不要声明相同的字重和样式。 浏览器会按自己的规则选一个,结果不可控。 只有一档的字,就只声明这一档。 不要把一份 Bold 文件声明成 400,也不要给只有一档的字加 font-weight: bold。可以用 font-synthesis: none 禁止浏览器人工加粗和拉斜,见《在 CSS 里使用:字体名、备用字体和多字重》。 可变字体写一个范围 可变字体一个文件包含连续的字重,用两个值写范围: @font-face { font-family: "My Sans VF"; src: url("my-sans-vf.woff2") format("woff2"); font-weight: 100 900; } local() 的正确写法 local() 让浏览器先找用户电脑上装好的同名字体,找到了就不下载。写法要点: 括号里写字体的完整名称(Full Name)或 PostScript 名,不是家族名; 一个 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 就够了。 参考资料4@font-face 的 src 描述符font-weightOpenType 规范:OS/2 表 加载策略:font-display、预连接和预加载 适合谁看:已经能用上网页字体,想让首屏更快、闪动更少的前端开发者。 font-display 怎么选 取值 等待字体(期间不显示文字) 换成新字体的时机 适合 auto 由浏览器决定,常见是 block — 不建议依赖 block 较长,规范建议约 3 秒 字体到了就换 图标字体等不能用别的字体代替的场合 swap 极短 字体到了就换,不限时间 中文正文、标题的默认选择 fallback 极短 约 3 秒内到了才换,晚了这次就不换 正文,希望减少晚到的跳动 optional 极短 基本只在字体已缓存时使用 字体可有可无、最在意稳定的页面 中文字体体积大、首访很难在 100 毫秒内到达, optional 的结果常常是首访看不到字体。一般选 swap,再用《减少跳动:后备字体和尺寸调整》 里的方法降低跳动。 预连接 字体放在另一个域名时,在 前部加: 浏览器会提前完成 DNS、TCP、TLS 握手。字体请求是匿名跨域请求,所以要带 crossorigin。 预加载:只用在确定的那一个文件上 适合:用固定文字裁出来的标题字体、整个西文字体文件等,确定首屏一定会用到的文件。 不适合:按 unicode-range 分片的中文字体。页面需要哪几片,要等排版时才知道,手动预加载的那一片多半用不上,反而和关键资源抢带宽。 预加载的字体请求即使同源也必须带 crossorigin,否则会下载两次。 用 JavaScript 控制(可选) 字体加载 API 可以检查和等待字体: // 等页面上所有字体加载完成 await document.fonts.ready; // 主动加载某个字重 await document.fonts.load('700 1em "My Sans"'); // 检查是否已可用 document.fonts.check('16px "My Sans"'); 适合「字体到了再执行动画」「截图前确保字体已加载」这类场景。普通页面用 CSS 就够了。 缓存 字体文件很少变化,建议给它们设置长期缓存(例如一年),文件内容变了就换文件名。 参考资料5CSS Fonts Module Level 4优化网页字体rel=preconnect 减少跳动:后备字体和尺寸调整 适合谁看:用了 font-display: swap,但字体换上来时页面「跳一下」,想减少布局偏移(CLS)的开发者。 为什么会跳 备用字体和网页字体的字宽、上下留白不同。换字体时,一行能放下的字数变了,行高也可能变了,后面的内容就整体移动。 对中文来说: 汉字基本都是等宽的方块,中文部分的字宽差别不大; 差别主要来自西文、数字、标点的宽度,以及行高; 如果没有写死 line-height,浏览器按字体自带的上下高度计算行高( line-height: normal),换字体后每行高度都可能变化。 第一步:写死行高 给正文写一个明确的行高,行高就不再依赖字体: body { line-height: 1.75; } 这是最简单、效果最明显的一步。排版上的行高建议见《正文用字:字号、行距和字重》。 第二步:选接近的备用字体 备用字体和网页字体越接近,跳动越小: 黑体类网页字体,备用写系统黑体(苹方、微软雅黑、思源黑体 / Noto Sans CJK); 宋体、楷体、仿宋类,备用写系统同类字体,见《中文字体栈怎么写》。 第三步:用尺寸调整描述符微调 可以为备用字体单独写一条 @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 正式版尚未支持。不支持的浏览器会忽略这些描述符,不会出错。 怎么量 Chrome 开发者工具「性能」面板,或 Lighthouse,查看 CLS(累计布局偏移); 对比开启和关闭网页字体时的截图。 参考资料4@font-face 的 CSS size-adjust@font-face 的 size-adjust 描述符@font-face 的 ascent-override 描述符 中文字体栈怎么写 适合谁看:要给中文网站写 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; 几个要点 西文字体写在中文字体前面,可以让英文、数字用西文字体,中文落到后面的中文字体上。反过来写,中文字体自带的西文会被用上。 最后一定要写通用族名: sans-serif、 serif、 monospace。 名字有空格要加引号。 英文名比中文名可靠。 部分浏览器和系统只认英文名,写了中文名(如「微软雅黑」)可以和英文名并列,但不要只写中文名。 system-ui 会用系统界面字体,在中文环境下效果因系统而异,用在界面组件上比用在长文正文上更合适。 缺字的情况 生僻字、新收录的扩展区汉字,系统字体也可能没有。要保证这些字能显示,可以在字体栈末尾加一款覆盖范围大的字体,见《网页和系统里怎么显示生僻字》。 参考资料4font-familyWindows 11 font listSystem Fonts 用文风网页字体:一页看懂正确接法 适合谁看:准备在网站上用文风网页字体,想一次把关键做法看全的开发者和站长。 本页是要点汇总,细节在《文风开发者文档》各页。 地址 https://cn.windfonts.com/fonts-packages/{目录名}/{字重}/{子集}/result.css {目录名}:字体页网址最后一段,小写; {字重}:照抄字体页上的字重名,区分大小写; {子集}: full、 zh、 zh-common、 en。只有 full 每款字都有,其他子集用前先在浏览器打开确认。 详见《网页字体地址规则》。 推荐的引入代码 第一行授权注释请保留,见《保留授权注释》; 预连接 cn.windfonts.com; 用 引入,首屏字体不要用 @import。 字体名:从 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 的数值: 数值不同,就可以放心一起用,页面里用 font-weight 切换; 数值相同,只引入其中一份,否则后引入的会覆盖先引入的; 只有一档的字,不要再加粗、加斜,可以加 font-synthesis: none。 详见《在 CSS 里使用:字体名、备用字体和多字重》。 已经帮你做好的事 字体已经按 unicode-range 拆成很多 WOFF2 分片,浏览器只下载用到的; CSS 里每条 @font-face 都写了 font-display: swap; CSS 和分片都允许跨域引用,并带长期缓存头。 不需要、也不建议做的事 不要预加载某个分片,原因见《加载策略:font-display、预连接和预加载》; 不要把 CSS 里的分片地址抄到自己的 CSS 里,分片文件名可能随字体更新而变化; 想放在自己服务器上,用整套切片包,见《自托管切片与 WordPress 主题》。 先看许可 文风收录的字体许可各不相同。上线前打开字体页的许可原文,确认网页使用在许可范围内,见《这款字能不能商用:五步自查》。 参考资料2文风字体库授权查询 自托管网页字体的许可要点 适合谁看:打算把字体文件放在自己服务器上做网页字体的开发者、站长和企业法务。 网页字体是一种「分发」 网页字体的原理是让访客的浏览器下载字体文件。从许可的角度看,你在把字体文件提供给每一位访客。所以: 商业字体:桌面授权一般不包括网页使用,要买网页授权。网页授权常按访问量、域名或年费计价,以厂商条款为准; 开源字体:OFL 明确允许用 @font-face 做网页字体(OFL-FAQ 2.1),但要满足许可条件; 厂商免费字体:有的允许网页使用,有的要求只能从官网下载、不得再分发或修改,要逐条看。 OFL 字体自托管的四个要点 保留版权和许可信息。 字体文件内部的版权、许可字段不要删;拆子集时加上保留名称表的选项,见《动手拆子集:pyftsubset》。 转 WOFF2 不一定算修改,拆子集一定算修改。 只做 WOFF2 压缩、数据和元数据完整保留,可以不算修改(OFL-FAQ 2.2);拆子集、删掉排版功能都是修改(OFL-FAQ 2.6)。 修改版不能用保留字体名。 字体声明了保留名的,拆过子集的版本要改名,除非能做到「功能等价」或者和作者另有约定(OFL-FAQ 2.7、2.8)。 网站条款不能收紧 OFL。 你可以对字体文件做防盗链,但不能宣称别人不能按 OFL 使用这款字(OFL-FAQ 1.17、1.24)。 逐条解读见《SIL 开放字体许可证(OFL)逐条读》。 改名怎么改 改的是字体文件内部的名字(名称表里的家族名、完整名、PostScript 名),以及你 CSS 里 font-family 的名字。可以用 fontTools 修改,见《fontTools 入门:查看、修改、转格式》。新名字不要包含原字体的保留名。 页面上要不要署名 OFL 不强制在网页上署名(OFL-FAQ 1.1.2)。但在网站的「关于」或页脚写明所用字体和许可,对访客和作者都友好。 使用文风的切片包 文风提供部分字体的切片包,许可仍然以每款字的许可原文为准,切片包里的授权注释请保留,见《自托管切片与 WordPress 主题》。 参考资料3OFL 常见问题(OFL-FAQ)Webfonts and Reserved Font Namessubset: Generate subsets of fonts or optimize file sizes 字体没生效?一套排查顺序 适合谁看:网页字体「好像没用上」「只有部分字变了」「变粗了」,想自己查原因的开发者。 文风网页字体的专门问答见《开发常见问题》。本页是通用的排查顺序,自托管、第三方字体也适用。 1. 看渲染字体 Chrome 或 Edge:开发者工具 →「元素」→ 选中文字 →「计算样式」面板拉到底,看「渲染字体(Rendered Fonts)」。它列出这段文字实际用了哪些字体、各显示了多少个字。 Firefox:开发者工具 →「检查器」→「字体」面板,可以看到页面用到的字体和来源。 显示的是系统字体 → 网页字体没加载或名字没对上,继续往下查; 网页字体和系统字体各占一部分 → 缺字或分片没加载完,见第 4 步。 2. 看网络请求 「网络」面板筛选「字体」和「CSS」,刷新: 看到的情况 可能原因 没有任何字体请求 font-family 名字和 @font-face 不一致;或页面上没有文字用到它 404 地址拼错,大小写不对,或子集不存在 请求被拦截,控制台有 CORS 报错 字体服务器没有返回允许跨域的响应头 200,但文件很小或类型不对 服务器返回了错误页面,打开地址看看内容 3. 核对名字 font-family 里写的名字必须和 @font-face 里声明的完全一致(大小写、空格、连字符)。用第三方 CSS 时,打开 CSS 复制名字,不要凭印象写。 4. 部分字是别的字体 用了只含常用字或只含中文的子集,生僻字、西文落到了备用字体; 字体本身没有这个字,见《繁体、日文、生僻字显示成方框怎么办》; 分片还在下载,等一会儿就会换掉。 5. 字被人工加粗或拉斜 页面要求的字重或斜体,在 @font-face 里没有对应的声明,浏览器就自己合成; 不同字重起了不同的家族名,见《写对 @font-face:字体名、字重和 local()》。 6. 用控制台确认 在开发者工具的控制台里运行: document.fonts.check('16px "字体名"') // true 表示已可用 [...document.fonts].map(f => [f.family, f.weight, f.status]) 7. 清缓存再试 字体更新后,浏览器和服务器缓存可能还是旧文件。用无痕窗口或「停用缓存」再试一次。 参考资料3Chrome 开发者工具:CSS 功能参考Firefox 开发者工具文档:Edit fontsFontFaceSet.check() 可变字体和增量字体传输 适合谁看:关注网页字体新技术、想知道哪些现在能用、哪些还要等的开发者。 可变字体:现在就能用 可变字体(Variable Font)把一个家族的多个字重(以及宽度、倾斜等)放进一个文件,用「轴」连续调节。 浏览器支持已经很普遍; CSS 里用 font-weight: 100 900 声明范围,页面里可以用任意中间值,如 font-weight: 450; 也可以用 font-variation-settings 调节字体自定义的轴。 中文可变字体的取舍: 思源黑体、思源宋体等都提供可变字体版本; 一个可变字体文件通常比单个字重大,但比多个字重加起来小; 页面只用一两个字重时,单独的静态字重分片往往更省流量;用三个以上字重,或需要动画、细调时,可变字体更有优势; 可变字体同样可以拆子集、分片。 字重的设计知识见《多字重家族和可变字体怎么挑》。 增量字体传输(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)。 参考资料5Variable fonts guide思源黑体 GitHub 仓库思源宋体 GitHub 仓库