本文由张庭岑原创。
一, WOFF2 字体文件格式优化
W3C 标准: WOFF File Format 2.0
方案简介
WOFF2 (Web Open Font Format 2) 字体文件格式已经是 W3C 标准, 无需考虑兼容性问题.
WOFF2 通过优化编码格式和更先进的压缩算法使字体文件压缩率提升(WOFF2 相比TTF/OTF压缩率提升40%-60%,相比 WOFF1 压缩率提升 30%), 同时具备更快的客户端解析速度.
下面是主流字体文件格式对比
以下是常见 Web 字体格式的对比表格,综合了技术特性、兼容性及适用场景:
格式 | 全称 | 浏览器兼容性 | 压缩率 | 核心优势 | 主要缺点 |
---|---|---|---|---|---|
TTF | TrueType Font | 全平台支持(Windows、macOS、Linux、移动端) | 低 | 通用性强,支持 Unicode 和高级排版(连字、字距调整) | 文件体积大, 移动端加载慢 |
OTF | OpenType Font | 全平台支持 | 低 | 支持 PostScript 轮廓,适合专业排版(如印刷设计) | 文件体积大, 移动端加载慢 |
WOFF | Web Open Font Format | 主流浏览器(Chrome、Firefox、Edge、Safari) | 高(比 TTF/OTF 小 40%) | 专为 Web 设计,支持压缩和元数据(如许可证信息) | 压缩率低于 WOFF2, 不支持旧版浏览器 |
WOFF2 | Web Open Font Format 2.0 | 现代浏览器(Chrome 40+、Firefox 36+、Edge、Safari 12.1+) | 极高(比 WOFF 小 30%) | Brotli 压缩算法,体积最小;支持高级特性(如可变字体) | 不支持旧版浏览器 |
场景分析
在本地能控制字体文件格式的情况下, 都应该使用 WOFF2; 在使用字体平台的情况下, 基本都支持 WOFF2;
字体格式转换工具: 如附录;
二, CSS unicode-range
字体子集化
unicode-range
已经是 W3C 的 CSS 标准, 无需考虑兼容性问题. 不需要服务端支持;
W3C 标准: CSS Fonts Module Level 4
MDN 文档: unicode-range
方案简介
在 CSS 中, @font-face
规定只有在 unicode-range
属性范围内的文字使用该字体;
DOM 文字渲染过程命中了 unicode-range
范围, 才会下载该字体子集;
因此可以将一个字体拆分成多个字体子集, 根据每个字体子集的 unicode-range
设置多个 @font-face
应用实例
鸿蒙开发者官网 的鸿蒙字体使用了 unicode-range
字体子集化方案, 将同一字体拆分成 96 个子集, 不同字重也拆分成不同子集, 完整字体 WOFF2
体积为 8.1M, 首屏字体子集请求 44 个共计下载 1.1M 字体子集;
LESS 代码如下 (也可以自行调试鸿蒙开发者官网 )
@fontUrl: '@/assets/fonts/HarmonyOS/HarmonyOS_Sans';
@font-face {
font-family: HarmonyOSHans-Regular;
src: url("@{fontUrl}/HarmonyOS_Sans_SC_Light.1.woff2") format("woff2");
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range: U+ff03,U+ff04,U+ff07,U+ff0a,U+ff17-ff19,U+ff1c,U+ff1d,U+ff20-ff3a,U+ff3c,U+ff3e-ff5b,U+ff5d,U+ffe0-ffe4;
}
@font-face {
font-family: HarmonyOSHans-Regular;
src: url("@{fontUrl}/HarmonyOS_Sans_SC_Regular.1.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+ff03,U+ff04,U+ff07,U+ff0a,U+ff17-ff19,U+ff1c,U+ff1d,U+ff20-ff3a,U+ff3c,U+ff3e-ff5b,U+ff5d,U+ffe0-ffe4;
}
@font-face {
font-family: HarmonyOSHans-Regular;
src: url("@{fontUrl}/HarmonyOS_Sans_SC_Light.2.woff2") format("woff2");
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range: U+f92c,U+f979,U+fa11,U+fe30,U+fe31,U+fe33-fe44,U+fe49-fe52,U+fe54-fe57,U+fe59-fe66,U+fe68-fe6b;
}
@font-face {
font-family: HarmonyOSHans-Regular;
src: url("@{fontUrl}/HarmonyOS_Sans_SC_Regular.2.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+f92c,U+f979,U+fa11,U+fe30,U+fe31,U+fe33-fe44,U+fe49-fe52,U+fe54-fe57,U+fe59-fe66,U+fe68-fe6b;
}
...略...
@font-face {
font-family: HarmonyOSHans-Regular;
src: url("@{fontUrl}/HarmonyOS_Sans_SC_Light.96.woff2") format("woff2");
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range: U+df-e5,U+e7-ea,U+ec,U+ed,U+f1-f4,U+f6,U+f9,U+fa,U+fc,U+101,U+103,U+113,U+12b,U+148,U+14d,U+16b,U+1ce,U+1d0,U+300,U+301,U+1ebf,U+1ec7,U+3042,U+3044,U+3046,U+3048,U+304a-3055,U+3057,U+3059-305b,U+305d,U+305f-3061,U+3063-306b,U+306d-3073,U+3075,U+3076,U+3078,U+3079,U+307b,U+307e,U+307f,U+3081-308d,U+308f,U+3092,U+3093,U+30a1-30a4,U+30a6-30bb,U+30bd,U+30bf-30c1,U+30c3,U+30c4,U+30c6-30cb,U+30cd-30d7,U+30d9-30e1,U+30e3-30e7,U+30e9-30ed,U+30ef,U+30f3;
}
@font-face {
font-family: HarmonyOSHans-Regular;
src: url("@{fontUrl}/HarmonyOS_Sans_SC_Regular.96.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+df-e5,U+e7-ea,U+ec,U+ed,U+f1-f4,U+f6,U+f9,U+fa,U+fc,U+101,U+103,U+113,U+12b,U+148,U+14d,U+16b,U+1ce,U+1d0,U+300,U+301,U+1ebf,U+1ec7,U+3042,U+3044,U+3046,U+3048,U+304a-3055,U+3057,U+3059-305b,U+305d,U+305f-3061,U+3063-306b,U+306d-3073,U+3075,U+3076,U+3078,U+3079,U+307b,U+307e,U+307f,U+3081-308d,U+308f,U+3092,U+3093,U+30a1-30a4,U+30a6-30bb,U+30bd,U+30bf-30c1,U+30c3,U+30c4,U+30c6-30cb,U+30cd-30d7,U+30d9-30e1,U+30e3-30e7,U+30e9-30ed,U+30ef,U+30f3;
}
场景分析
- 通过良好的拆分字体子集, 可以减少首屏加载字体文件的体积; 大部分情况下,
unicode-range
字体子集化方案都可以减少 40% 到 70% 的字体文件体积; - 在多语言和中文场景, 对性能提升有显著效果;
- 同时不应该忽略的是字体子集化会增加网络请求的数量, 应该避免造成负向优化;
三, 增量字体传输方案 Incremental Font Transfer (IFT)
W3C 标准(草稿阶段): Incremental Font Transfer
方案简介
IFT 处于 W3C 标准的草案阶段, 将字体文件类比成 video
, 将 unicode
类比成 video
的时间轴, 让浏览器自动下载所需要的字体子集;
从高层次来看,增量字体的使用方式如下:
- 客户端下载初始字体文件,其中包含来自字体完整版本的一些初始数据子集以及描述可用于扩展字体的补丁集的嵌入数据。
- 客户端会根据待渲染的内容选择、下载并应用补丁来扩展字体,使其涵盖更多字符、布局特性和/或变体空间。每次有新内容时,都会重复此步骤。
下面我用比较形象的两个图展示:
Video 播放根据时间切片进行增量传输, 最开始只需要下载时间轴和 video
的元数据即可, 包含时间轴和 video 切片的对应关系, 播放过程会自动下载 video 切片;
字体根据 unicode 进行增量传输, 最开始只需要下载 unicode
和字体子集的对应关系即可, DOM 渲染过程会自动请求增量字体
W3C 标准规定了增量字体的编码方式, 以便客户端代理或者服务端代理可以返回增量子集;
客户端将自动识别增量字体并请求增量子集, 前端开发人员未来就不用操心字体体积优化了;
目前该提案还在草案阶段, 完全自动的字体优化还未能实现; 因此后面的方案都是自行实现的增量字体传输方案;
四, Google Fonts 字体按需加载
Google Fonts, Typekit 等字体平台都提供 Web Font 服务, 目前发现 Google Fonts 不进提供字体全量下载, WOFF2 字体格式, 字体子集下载等服务, 还提供字体按需加载能力;
向 API 传入 text
参数, 将只下载参数 text
内包含的字体子集
方案简介
- 前端动态进行网页内容识别, 输出网页需要渲染的文字:
textContent
- 根据
textContent
生成字体子集的@font-face
并插入文档流:@font-face
的src
的url
指向https://google-font-api?text=textContent
- 浏览器 DOM 文档渲染命中
@font-face
规则, 向 google fonts 服务端请求下载字体子集 Google Fonts
服务端返回仅包含textContent
的字体子集, 浏览器重新渲染文字
按需加载字体子集的思路, 不一定要生成 @font-face
CSS, 还可以使用 FontFace Api 直接下载字体子集, 例如:
// 代码片段来自古茗团队博客: [因网速太慢我把20M+的字体压缩到了几KB](https://juejin.cn/post/7490337281866317836)
const text = textContent;
// 构建字体对象
const font = new FontFace(
fontName,
`url(http://127.0.0.1:5000/font/${fontName}?text=${text}&format=woff2)`
);
// 加载字体
font.load();
// 添加到文档字体集中
document.fonts.add(font);
场景分析
- 在运营场景, 首屏存在多种字体, 并且只有特定元素如 banner, logo, slogan 等才使用的字体, 按需加载可以带来大幅度的性能提升和用户体验提升;
- 在服务端渲染页面使用字体按需加载将获得极致字体性能
- 在需要处理用户中文输入的情况下, 体验不佳 - 会发起大量单个文字的字体子集请求, 并且页面文字渲染可能发生跳变 (用户输入场景, unicode-range 方案浏览器会自动下载字体子集; 按需加载方案需要我们主动监听文档流变化)
- 不一定依赖服务端, 在活动页, 运营页等场景, 可以 !!提前!! !!按需!! 生成字体子集即可;
技术细节
- 识别网页渲染内容的算法: nathanford/google-fonts-dynamic-subsetter
- 动态监听网页内容变化的算法需要自行判断是否需要动态监听: MDN - MutationObserver
- 服务端按需生成字体子集的工具: 如附录
工具附录
工具 | 连接 | 说明 |
---|---|---|
fontmin | https://ecomfe.github.io/fontmin/#banner | baidu EFE 团队出品的字体子集化工具, 基于 nodejs 编写, 有客户端, cli, 还有 nodejs api 可以基于它做字体按需加载服务端 |
fonttools | https://github.com/fonttools/fonttools | 一个使用 python 操作字体文件的库, 可以将字体文件在各种编码之间快速转换, 也可以拆分字体子集 |
glyphhanger | https://www.zachleat.com/web/glyphhanger/ | - 使用 puppeteer 爬取网页所有的文字, 输出 unicode-range 便于拆分字体子集; - 可以根据 unicode-range 将输入的字体文件拆分出字体子集 (这部分功能封装的是 fonttools ) |
font-spider | https://github.com/aui/font-spider | 功能同 glyphhanger , 同样使用 puppeteer 爬取网页内容生成 unicode-range , 并且可以将字体拆分出字体子集, 不同的是 font-spider 有 webpack/grunt/gulp 插件, 可以在本地构建阶段自动化地为 SPA 打包处最小量的字体子集, font-spider 内部封装的是 fontmin |
glyphhanger 使用示例: 爬取网页, 按需输出字体子集
关于OpenTiny
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:https://opentiny.design
OpenTiny 代码仓库:https://github.com/opentiny
TinyVue 源码:https://github.com/opentiny/tiny-vue
TinyEngine 源码: https://github.com/opentiny/tiny-engine
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~
如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~