中文字体的终极解决方案——对字体进行切片

2023-04-24coding中文字体

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

  1. 安装
npm install --save-dev font-slice
yarn add -D font-slice
  1. 使用
const createFontSlice = require('font-slice');

createFontSlice({
  // fontPath
  fontPath: path.resolve(__dirname, 'YourPath.ttf'),
  // outputDir
  outputDir: path.resolve(__dirname, './output'),
})

可能等待时间较长,请耐心等待,完成后可以直接预览字体。

  1. 引用生成的 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 的加载原理。

image 20210619122937737 image 20210621224958862

即它采用了机器学习等手段,将字体拆分成合适的粒度,比如把一个 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 来分割字体的示例,那么这个份分割字体的实例,是不是能对所有的中文字体都生效呢。

我是这么想的,也这么尝试了下,于是就有了这个项目。

该项目所做的内容如下:

  1. 提取 google fonts 的 unicode-range。

  2. 提取要处理的字体包含的所有字符,得到 google fonts 的 unicode-range 和字体里包含的字符的交集部分。

  3. 将字符按照上面步骤得出的拆分方案,提取字体子集,生成多个文件及 css 样式文件。

最终展现出的效果还不错。

这个方案和我前文的提取字符子集的方法各有各的适用空间。

  • 提取子集可以实现极致的体积压缩,在仅需展示少量字体的情况下效果显著。

  • 字体切片可以在支持所有字体的情况下,以较小的加载体积呈现页面。

欢迎你使用以上两种方案,如果遇到问题可以联系我或者提 issue 处理~

感谢

点赞 0