前端工程师偶尔会开发带有中文自定义字体的页面,这些字体可能是客户端系统上不存在的,这时候就要通过@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,但要做好工程化,手动操作容易翻车。
对于动态内容,我们可以分以下几种情况考虑:
-
截取常用3500字和特殊需求文字,不可避免有些字不会被截取,适用要求不太严格的场景
-
(思路)算法统计产品常用汉字,动态更新前端使用的字体子集
-
服务端统计页面中文案,截取子集,上传CND,页面插入CSS,会造成TTFB变长
-
前端统计页面中字体,调用API截取字体,插入CSS,可能会出现FOUT(flash of unstyle text)
读到这你可能会觉得麻烦,为什么一定要使用自定义字体,使用系统上自带的字体不行吗?如果你确实没有绝对的必要使用自定义字体,使用系统字体确实是比较高效的方式。
充分利用系统字体
使用系统字体的好处有很多,最关键还是这三个:
-
无需下载,节省带宽
-
没有FOIT和FOUT,页面文字不会闪烁
-
系统自带字体在系统上视觉效果很好
我们来看看别人家的网站是怎么使用系统字体的
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 DisplayBlinkMacSystemFont
Mac OSX 上的 chrome 应用系统默认字体Segoe UI
window 和 window phone 上的字体Roboto
Android 和较新的 Chrome OS。放在Segoe UI之后列出,如果是Windows上的Android开发人员并安装了Roboto,则将改用Segoe UIsystem-ui
命中系统UI默认字体
这五个字体族分别命不同系统自带的字体,覆盖了大部分常见平台,可以说是一个终极解决方案了,看完这部分后反思一下,如果你的网站运行在某个国家的不常用的平台上,能给用户带来良好的阅读体验吗?
参考资料
🌻感谢李松峰老师之前关于字体的分享,帮助我在相关的实践上节省了不少时间。后来无意中在normalize.css中看到了这个issue,又补充上了使用系统字体部分的内容。