盘点字体性能优化方案

本文由张庭岑原创。

一, 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 代码如下 (也可以自行调试鸿蒙开发者官网 )

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;
}

场景分析

  1. 通过良好的拆分字体子集, 可以减少首屏加载字体文件的体积; 大部分情况下, unicode-range 字体子集化方案都可以减少 40%70% 的字体文件体积;
  2. 在多语言和中文场景, 对性能提升有显著效果;
  3. 同时不应该忽略的是字体子集化会增加网络请求的数量, 应该避免造成负向优化;

三, 增量字体传输方案 Incremental Font Transfer (IFT)

W3C 标准(草稿阶段): Incremental Font Transfer

方案简介

IFT 处于 W3C 标准的草案阶段, 将字体文件类比成 video, 将 unicode 类比成 video 的时间轴, 让浏览器自动下载所需要的字体子集;

从高层次来看,增量字体的使用方式如下:

  1. 客户端下载初始字体文件,其中包含来自字体完整版本的一些初始数据子集以及描述可用于扩展字体的补丁集的嵌入数据。
  2. 客户端会根据待渲染的内容选择、下载并应用补丁来扩展字体,使其涵盖更多字符、布局特性和/或变体空间。每次有新内容时,都会重复此步骤。

下面我用比较形象的两个图展示: Video 播放根据时间切片进行增量传输 , 最开始只需要下载时间轴和 video 的元数据即可, 包含时间轴和 video 切片的对应关系, 播放过程会自动下载 video 切片;

字体根据 unicode 进行增量传输 , 最开始只需要下载 unicode 和字体子集的对应关系即可, DOM 渲染过程会自动请求增量字体

W3C 标准规定了增量字体的编码方式, 以便客户端代理或者服务端代理可以返回增量子集;

客户端将自动识别增量字体并请求增量子集, 前端开发人员未来就不用操心字体体积优化了;

目前该提案还在草案阶段, 完全自动的字体优化还未能实现; 因此后面的方案都是自行实现的增量字体传输方案;

四, Google Fonts 字体按需加载

Google Fonts, Typekit 等字体平台都提供 Web Font 服务, 目前发现 Google Fonts 不进提供字体全量下载, WOFF2 字体格式, 字体子集下载等服务, 还提供字体按需加载能力;

Google Fonts API:

向 API 传入 text 参数, 将只下载参数 text 内包含的字体子集

方案简介

  1. 前端动态进行网页内容识别 , 输出网页需要渲染的文字: textContent
  2. 根据 textContent 生成字体子集的 @font-face 并插入文档流: @font-facesrcurl 指向 https://google-font-api?text=textContent
  3. 浏览器 DOM 文档渲染命中 @font-face 规则, 向 google fonts 服务端请求下载字体子集
  4. Google Fonts 服务端返回仅包含 textContent 的字体子集, 浏览器重新渲染文字

按需加载字体子集的思路, 不一定要生成 @font-face CSS, 还可以使用 FontFace Api 直接下载字体子集, 例如:

javascript 复制代码
// 代码片段来自古茗团队博客: [因网速太慢我把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);

场景分析

  1. 在运营场景, 首屏存在多种字体, 并且只有特定元素如 banner, logo, slogan 等才使用的字体, 按需加载 可以带来大幅度的性能提升和用户体验提升;
  2. 服务端渲染 页面使用字体按需加载将获得极致字体性能
  3. 在需要处理用户中文输入 的情况下, 体验不佳 - 会发起大量单个文字的字体子集请求 , 并且页面文字渲染可能发生跳变 (用户输入场景, unicode-range 方案浏览器会自动下载字体子集; 按需加载方案需要我们主动监听文档流变化)
  4. 不一定依赖服务端 , 在活动页 , 运营页 等场景, 可以 !!提前!! !!按需!! 生成字体子集即可;

技术细节

  1. 识别网页渲染内容的算法: nathanford/google-fonts-dynamic-subsetter
  2. 动态监听网页内容变化的算法需要自行判断是否需要动态监听: MDN - MutationObserver
  3. 服务端按需生成字体子集的工具: 如附录

工具附录

工具 连接 说明
fontmin ecomfe.github.io/fontmin/#ba... baidu EFE 团队出品的字体子集化工具, 基于 nodejs 编写, 有客户端 , cli , 还有 nodejs api 可以基于它做字体按需加载服务端
fonttools github.com/fonttools/f... 一个使用 python 操作字体文件的库, 可以将字体文件在各种编码之间快速转换, 也可以拆分字体子集
glyphhanger www.zachleat.com/web/glyphha... - 使用 puppeteer 爬取网页所有的文字, 输出 unicode-range 便于拆分字体子集; - 可以根据 unicode-range 将输入的字体文件拆分出字体子集 (这部分功能封装的是 fonttools)
font-spider github.com/aui/font-sp... 功能同 glyphhanger, 同样使用 puppeteer 爬取网页内容生成 unicode-range, 并且可以将字体拆分出字体子集, 不同的是 font-spiderwebpack/grunt/gulp 插件, 可以在本地构建阶段自动化地为 SPA 打包处最小量的字体子集, font-spider 内部封装的是 fontmin

glyphhanger 使用示例: 爬取网页, 按需输出字体子集

关于OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网opentiny.design OpenTiny 代码仓库github.com/opentiny TinyVue 源码github.com/opentiny/ti... TinyEngine 源码: github.com/opentiny/ti... 欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

相关推荐
林太白5 分钟前
Zustand状态库(简洁、强大、易用的React状态管理工具)
前端·javascript·react.js
Juchecar9 分钟前
Vue3 模板引用 useTemplateRef 详解
前端·vue.js
鼓浪屿11 分钟前
vue3的组件通信方式
前端
念旧Zestia29 分钟前
Oxc 家族 vs Biome——定位、能力与底层差异综述
前端
YuJie30 分钟前
vue3 无缝滚动
前端·javascript·vue.js
Juchecar30 分钟前
Vue3 表单输入 v-model 指令详解
前端·vue.js
晴空雨38 分钟前
Emmet 完全指南:让 HTML/CSS 开发效率提升 10 倍
前端·html
小野鲜40 分钟前
前端打开新的独立标签页面,并且指定标签页的大小,管理新标签页面的打开和关闭(包含源码和使用文档)
前端·javascript
一枚前端小能手40 分钟前
🌐 Web应用也想有原生App的体验,PWA来实现
前端·pwa
十五_在努力1 小时前
参透 JavaScript —— 解析浅拷贝、深拷贝及手写实现
前端·javascript