不只是 @font-face
字体文件在项目中的应用可以说是非常普遍的。例如使用第三方字体,以 iconfont 形式存在的字体图标等等。
但是加载这些字体文件可不是简单一句 @font-face
就完事了。
本文主要参考 comprehensive-webfonts,将介绍加载字体存在的问题和最佳实践。
恼人的 FOIT
首屏加载中的 FOUC(Flash of Unstyled Content) 是需要极力避免的,为此我们常常将关键路径使用的样式内联到 HTML 中。 同样的,字体文件在加载完成之前,浏览器会使用缺省字体显示内容,这就是 FOUT(Flash of Unstyled Text)。 但是问题就出在这里,除了 IE,其他浏览器都会等待 3 秒才展示系统字体,这就造成了 3 秒的 FOIT(Flash of Invisible Text)。用户不得不面对长达 3 秒消失了文字的内容。
font-display
为了让浏览器立即使用缺省字体现实内容,可以使用 font-display 这个 CSS 新属性。
立即使用缺省字体,Web 字体加载完成后立即更换,重新渲染:
font-display: swap;
这里有一个 Chrome Devtool 实战视频,需翻墙。 一切都很美好,缺点是浏览器支持度。
内联字体
既然担心字体文件过大,影响加载时间,那么使用 data uri 内联是个不错的选择,可以完全避免 FOUT 和 FOIT。
@font-face {
font-family: Lato;
src: url('data:application/x-font-woff;charset=utf-8;base64...');
}
但是存在以下问题:
- 影响首屏性能。毕竟将字体也引入到了关键路径之中,需要谨慎选择文件的大小和数量。
- 由于完全指定了使用的字体,
@font-face
特有的提供多种文件格式供浏览器选择就无法使用了。 - 多个字体无法并行加载。
为了避免 CSS 阻塞渲染,之前《让骨架屏更快渲染》一文使用了 JS 异步加载 CSS,这里也可以借鉴下:
<script>
/*! loadCSS: load a CSS file asynchronously*/
(function(e){"use strict";var t=function(t,n,r)});
loadCSS("async-data-uri.css");
</script>
当然这并不能解决 2,3 两点。
CSS Font Loading API
为了增加开发者对于字体加载状态的控制,目前处于草案阶段的 CSS Font Loading API 是值得关注的。
从草案中提供的 API 示例来看,我们可以得知页面中使用的全部字体何时加载完成:
document.fonts.ready.then(function() {
// 全部加载完成后显示
var content = document.getElementById("content");
content.style.visibility = "visible";
});
通过 load
加载指定字体:
function drawStuff() {
var ctx = document.getElementById("c").getContext("2d");
ctx.fillStyle = "red";
ctx.font = "50px MyDownloadableFont";
ctx.fillText("Hello!", 100, 100);
}
document.fonts.load("50px MyDownloadableFont")
.then(drawStuff, handleError);
第三方字体加载库
又到了熟悉的 polyfill 出场了。我们可以借助第三方字体加载库,在加载完成后,在根元素上应用 CSS Class,实现 FOUT + swap 的效果。
首先正常使用 @font-face
:
@font-face {
font-family: Lato;
src: url('font-lato/lato-regular-webfont.woff2') format('woff2'),
url('font-lato/lato-regular-webfont.woff') format('woff');
}
.fonts-loaded body {
font-family: Lato, sans-serif;
}
使用 Font Face Observer,这里可以使用一个小技巧避免多次访问重复请求字体文件:
var fontA = new FontFaceObserver('Lato');
var fontB = new FontFaceObserver('LatoBold', {
weight: 700
});
Promise.all([
fontA.load(null, 10000),
fontB.load(null, 10000)
]).then(function () {
document.documentElement.className += " fonts-loaded";
// 加载完成后添加标记,后续访问时可以以此判断不重复加载字体
sessionStorage.fontsLoadedFoutWithClassPolyfill = true;
});
使用类似思路的还有更有名的 Web Font Loader。 后续的方案也都基于 CSS Font Loading API 及其 polyfill。
分阶段加载
在英文字体中,同一个字体常常会有多个变体,例如加粗,斜体等等。渐进式地加载这些字体,能够减少 FOIT 时间。
例如这里分成两个阶段,第一阶段仅仅加载一个基础字体,完成后立即应用展示。随后开始第二阶段全部字体的加载应用。
值得注意的是基础字体这里需要声明两遍 LatoInitial
和 Lato
:
@font-face {
font-family: LatoInitial;
src: url('font-lato/lato-regular-webfont.woff2') format('woff2'),
url('font-lato/lato-regular-webfont.woff') format('woff');
}
@font-face {
font-family: Lato;
src: url('font-lato/lato-regular-webfont.woff2') format('woff2'),
url('font-lato/lato-regular-webfont.woff') format('woff');
}
@font-face {
font-family: Lato;
src: url('font-lato/lato-bold-webfont.woff2') format('woff2'),
url('font-lato/lato-bold-webfont.woff') format('woff');
font-weight: 700;
}
body {
font-family: sans-serif;
}
.fonts-loaded-1 body {
font-family: LatoInitial;
}
.fonts-loaded-2 body {
font-family: Lato;
}
你可能会好奇 Lato
拥有两个字体资源 regular
和 bold
,在实际使用中浏览器会选择哪一个呢?
可以参考 Font selection algorithm & Font synthesis,这里就不展开了。
if( "fonts" in document ) {
if( sessionStorage.fontsLoadedFoft ) {
document.documentElement.className += " fonts-loaded-2";
return;
}
document.fonts.load("1em LatoInitial").then(function () {
document.documentElement.className += " fonts-loaded-1";
Promise.all([
document.fonts.load("400 1em Lato"),
document.fonts.load("700 1em Lato"),
document.fonts.load("italic 1em Lato"),
document.fonts.load("italic 700 1em Lato")
]).then(function () {
document.documentElement.className += " fonts-loaded-2";
sessionStorage.fontsLoadedFoft = true;
});
});
}
最佳实践!
其实到目前为止,已经称得上是比较理想的方案了。
对于英文字体,在第一阶段中加载的基础字体还可以精简,例如使用仅仅包含字母的 LatoSubset
以减少文件大小。
这还不算完,既然已经精简过,使用 data uri 增加的首屏加载时间将完全优于增加的网络开销。
Demo
关于字体图标的一点想法
使用 iconfont 的时候,其实是希望出现 FOIT,反而要避免 FOUT。否则首屏将出现一个大大的 search
文字。
<span class='material-icon'>search</span>
这时上述提到的方法虽然不能直接使用,但 CSS Font Loading API 还是能派上用场。 一种理想的做法是默认字体图标不可见,字体加载完成后变为可见。 可以参考 Typekit 中的例子。