Demo
先来看下 Demo 吧,在下面的输入框里,你可以输入任何字哦,它们都会以对应的字体加载。
- 得意黑字体 - 处理前 ttf 大小 2.1M。
- Aa楷宋(个人非商用版本) 处理前大小 4.5M
- ark-pixel-font 处理前 ttf 大小 2.5 M,woff2 大小 324 KB,(这里如果有字体不支持的话就是原像素字体暂不支持)
实际请求的字体资源包大小可以在 devtools - Network 中看到,具体加载的大小与页面中使用的字符相关。
一般情况下,只需要加载少于 30% 的内容即可完整展示一个页面。
使用
github 地址: https://github.com/voderl/font-slice
- 安装
npm install --save-dev font-slice
yarn add -D font-slice
- 使用
const createFontSlice = require('font-slice');
createFontSlice({
// fontPath
fontPath: path.resolve(__dirname, 'YourPath.ttf'),
// outputDir
outputDir: path.resolve(__dirname, './output'),
})
可能等待时间较长,请耐心等待,完成后可以直接预览字体。
- 引用生成的 font.css 文件,设置对应的 fontFamily 即可
将生成的产物部署到 cdn 上,直接引用 cdn 的地址就可以了。
更多配置项请前往 github 页查看。
注意项:
-
默认的 font-display 为 swap,即在字体没有加载完成时,先使用别的字体展示。需要调整的话可以在传入的 options 里指明。如果设置为 block,当字体没有加载完成时,会在一定的时间里不展示对应的内容。 更多 font-display 介绍请看这里
-
同时建议在 cdn 中将对应的字体目录直接设置一定时长的浏览器缓存,避免因字体加载导致页面内容闪动。
-
如果在 canvas 中使用,需要先加载文案对应的字体子集再去渲染
// 字体引入 css 文件需要先加载完成 document.fonts.load(`14px ${fontFamily}`, '指定文案').then(() => { ctx.fillText('指定文案'); });
数据
以得意黑字体为例为例:
处理前 ttf 大小 2074KB,woff2 大小 928KB.
处理后每个类型的字体生成 95 个文件:
ttf 总大小为 2.3M (最小文件 3.4K,最大文件 55K)
woff2 总大小为 1.3M (最小文件 1.5K,最大文件 33K)
实际加载页面的体积由页面使用的字符决定,以该页面为例,只需要加载 386KB 就能覆盖全部字符。
如果使用上面的像素字体 ark-pixel-font, 只需要加载 133 KB 资源即可覆盖本页面。
原理
以上的效果是怎么做到的呢?
很久之前我就写过一篇中文字体的解决方案,对中文字体进行压缩。
这篇文章主要讲的是扫描你的代码及博文中所用到的汉字,然后提取字体文件的子集,从而达到一个比较小的字体加载体积。
但这样的方法,在面对用户自定义输入、比如评论等行为时就处理不了了。
在那篇文章的开头,我解释了 Google Fonts 的加载原理。
即它采用了机器学习等手段,将字体拆分成合适的粒度,比如把一个 4MB 的字体包分成 100 个 40KB 的字体包,这样的话,一般网页中使用到的中文也只是一部分字体,只需要加载多个资源包就能完全覆盖。同时,就算网页中有很多生僻字,需要付出的代价也只是多加载几个资源包。
它的 css 引入文件示例如下:
@font-face {
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosanssc/v12/k3kXo84MPvpLmixcA63oeALhLOCT-xWNm8Hqd37g1OkDRZe7lR4sg1IzSy-MNbE9VH8V.4.woff2) format('woff2');
unicode-range: U+1f1e9-1f1f5, U+1f1f7-1f1ff, U+1f21a, U+1f232, U+1f234-1f237, U+1f250-1f251, U+1f300, U+1f302-1f308, U+1f30a-1f311, U+1f315, U+1f319-1f320, U+1f324, U+1f327, U+1f32a, U+1f32c-1f32d, U+1f330-1f357, U+1f359-1f37e;
}
/* [5] */
@font-face {
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosanssc/v12/k3kXo84MPvpLmixcA63oeALhLOCT-xWNm8Hqd37g1OkDRZe7lR4sg1IzSy-MNbE9VH8V.5.woff2) format('woff2');
unicode-range: U+fee3, U+fef3, U+ff03-ff04, U+ff07, U+ff0a, U+ff17-ff19, U+ff1c-ff1d, U+ff20-ff3a, U+ff3c, U+ff3e-ff5b, U+ff5d, U+ff61-ff65, U+ff67-ff6a, U+ff6c, U+ff6f-ff78, U+ff7a-ff7d, U+ff80-ff84, U+ff86, U+ff89-ff8e, U+ff92, U+ff97-ff9b, U+ff9d-ff9f, U+ffe0-ffe4, U+ffe6, U+ffe9, U+ffeb, U+ffed, U+fffc, U+1f004, U+1f170-1f171, U+1f192-1f195, U+1f198-1f19a, U+1f1e6-1f1e8;
}
...
它实现了一套通过 unicode-range 来分割字体的示例,那么这个份分割字体的实例,是不是能对所有的中文字体都生效呢。
我是这么想的,也这么尝试了下,于是就有了这个项目。
该项目所做的内容如下:
-
提取 google fonts 的 unicode-range。
-
提取要处理的字体包含的所有字符,得到 google fonts 的 unicode-range 和字体里包含的字符的交集部分。
-
将字符按照上面步骤得出的拆分方案,提取字体子集,生成多个文件及 css 样式文件。
最终展现出的效果还不错。
这个方案和我前文的提取字符子集的方法各有各的适用空间。
-
提取子集可以实现极致的体积压缩,在仅需展示少量字体的情况下效果显著。
-
字体切片可以在支持所有字体的情况下,以较小的加载体积呈现页面。
欢迎你使用以上两种方案,如果遇到问题可以联系我或者提 issue 处理~