前端字体使用实践

1,664 阅读6分钟

前端工程师偶尔会开发带有中文自定义字体的页面,这些字体可能是客户端系统上不存在的,这时候就要通过@font-face将自定义字体下载到本地,但中文字体文件是很大的,动辄超过10M,相比英文字体文件几十K来说算是庞然大物了。如果用户处在弱网环境下,加载完整的字体文件可能会出现FOUT的现象,体验不友好,而且还浪费带宽。

研究表明,3500常用中文汉字(中国义务教育9年级需要掌握的汉字数量)即可覆盖日常使用汉字的99.8%:

  • 500 字(78.53202%)
  • 1000字(91.91527%)
  • 1500字(96.47563%)
  • 2000字(98.38765%)
  • 2500字(99.24388%)
  • 3000字(99.63322%)
  • 3500字(99.82015%)

有一些我们认为常用的汉字并不在这3500字的范围内,比如天干地支。

既然常用3500汉字已经能覆盖99.82015%,我们可以考虑按需引入的方式,也就是使用字体截取,将我们需要的字体子集加入到页面中。

unicode-range

unicode-range是作用在@font-face中的一个属性,通过指定unicode码点的范围,决定字体文件中哪些文字会在页面中生效。下面的代码中,unicode-range中的码点是“全力以赴”四个字,'Source Han Serif'是思源宋体,如果对一段文本设置font-family: ‘Source Han Serif’,那么只有这四个字会应用这个字体。

@font-face {
  font-family: 'Source Han Serif';
  src: url() format('ttf'),
    url() format('woff'),
    url() format('woff2');
  unicode-range: u+5168,u+529b,u+4ee5,u+8d74;
}

使用unicode-range并不能改变实际下载字体文件的大小,如果指定的四个码点出现在内容中,浏览器会将整个字体文件下载下来,那unicode-range在字体截取中的作用是什么?

我们先来看一下@font-face触发字体文件下载的条件:

  • 合法的@font-face 规则,并且当前浏览器需要支持 src 列表中给出的格式
  • 文档中有节点使用了 @font-face 中相同的 font-family
  • 在 Webkit 和 Blink 引擎中,使用该 font-family 的节点内容不能为空
  • 如果 @font-face 中指定了 unicode-range,出现的文字内容还必须落在设定的 unicode 范围中

根据最后一点我们可以知道,如果unicode-range中指定的码点没有出现在网页内容中,字体文件是不会下载的。至少有一个码点出现在页面内容中,浏览器就会下载字体文件。

字体截取工具

我们可以通过使用字体截取工具,将指定文字从整个字体文件中截取成一个字体子集,下面是几个常用的字体截取工具:

类型 截取 爬虫 转base64 其他
glyphhanger cli
font-spider cli
fontmin mac客户端
transfonter web 源文件须小于10M

这里我们以glyphhanger为例介绍从网页中爬取指定文本截取成字体子集的操作:

glyphhanger http://127.0.0.1:8080/index.html 
--family='custom' # 分析目标页面中使用‘custom’字体的文字
--subset=SourceHanSerifCN-Light.ttf # 字体源文件
--formats=woff2   # 截取字体子集的文件格式
--css             # 生成css文件

最终会生成一个woff2格式的字体子集文件,一个包含@font-face代码的CSS文件。在实际的工程中只做到这一步是远远不够的,我们还要实现自动化,比如将截取的字体子集文件上传CDN并替换链接,或者将小于某个值的字体文件转base64内联,等等。

结合unicode-range和字体截取工具,我们可以实现只将页面需要的字体截取字体子集,并通过unicode-range指定子集中文字的码点,以做到完全按需加载。但这是比较理想的情况,如果实际项目中内容是用户输入的,那该怎么处理?

字体截取实践

在实际项目中,我们可以将静态内容和动态内容分开处理。

对于静态内容,我们可以通过爬虫截取工具将页面中的内容截取成子集,然后转base64内联或者上传CDN,但要做好工程化,手动操作容易翻车。

对于动态内容,我们可以分以下几种情况考虑:

  1. 截取常用3500字和特殊需求文字,不可避免有些字不会被截取,适用要求不太严格的场景

  2. (思路)算法统计产品常用汉字,动态更新前端使用的字体子集

  3. 服务端统计页面中文案,截取子集,上传CND,页面插入CSS,会造成TTFB变长

  4. 前端统计页面中字体,调用API截取字体,插入CSS,可能会出现FOUT(flash of unstyle text)

读到这你可能会觉得麻烦,为什么一定要使用自定义字体,使用系统上自带的字体不行吗?如果你确实没有绝对的必要使用自定义字体,使用系统字体确实是比较高效的方式。

充分利用系统字体

使用系统字体的好处有很多,最关键还是这三个:

  1. 无需下载,节省带宽

  2. 没有FOIT和FOUT,页面文字不会闪烁

  3. 系统自带字体在系统上视觉效果很好

我们来看看别人家的网站是怎么使用系统字体的

Github

font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;

Medium

font-family: medium-content-sans-serif-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;

Ant-Design

font-family: -apple-system, BlinkMacSystemFont, segoe ui,
Roboto, helvetica neue, Arial, noto sans, sans-serif,
apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;

随便找了平时最常用的三个网站,发现他们页面body上的font-family都重复出现了以下几种字体:

  • -apple-system Mac OS X 和 iOS 的 Safari 中命中 San Francisco,在老版本Mac OS X 中命中 Neue Helvetica 和 Lucida Grande。 会根据字体大小选择使用 San Francisco Text 或者 San Francisco Display
  • BlinkMacSystemFont Mac OSX 上的 chrome 应用系统默认字体
  • Segoe UI window 和 window phone 上的字体
  • Roboto Android 和较新的 Chrome OS。放在Segoe UI之后列出,如果是Windows上的Android开发人员并安装了Roboto,则将改用Segoe UI
  • system-ui命中系统UI默认字体

这五个字体族分别命不同系统自带的字体,覆盖了大部分常见平台,可以说是一个终极解决方案了,看完这部分后反思一下,如果你的网站运行在某个国家的不常用的平台上,能给用户带来良好的阅读体验吗?

参考资料

  1. 前端字体截取
  2. normal.css 665

🌻感谢李松峰老师之前关于字体的分享,帮助我在相关的实践上节省了不少时间。后来无意中在normalize.css中看到了这个issue,又补充上了使用系统字体部分的内容。