webfont解剖
- Unicode字体可以包含数以千计字形
- 有四个字体格式: WOFF2, WOFF, EOT, TTF
- 一些字体格式需要使用GZIP压缩
一个web字体是字形的集合,且每个字形是一个描述了一个字母亦或符号的矢量图。
所以,一个字体文件的大小由两个因素决定:每个字形矢量路径的复杂程度和每个字体所包含的字形数量。
例如,Open Sans, 最流行的web字体之一, 包含了897个字形,包含了拉丁,希腊和古代斯拉夫语字母。
当选择一个字体的时候,重要的是考虑哪些字符集被支持。
例如, Google’s Noto font family 目标是支持世界上所有的语言. 但是,注意Noto的大小,因为包括了所有的语言,所以压缩后大小是130MB+。
Web字体格式
网上如今四个web字体格式: EOT, TTF, WOFF, 和 WOFF2.
不幸的是,虽然有很多的选择,但是没有一个字体是可以在旧的浏览器和新的浏览器上通用的:EOT只适用于IE,TTP部分被IE支持。WOFF享有最广泛的支持但是在老的浏览器中不必支持,并且对于很多新的浏览器真在添加对于WOFF 2.0的支持。
所以,这意味着我们需要使用多个字体格式来保持良好的用户体验:
- 在支持WOFF 2.0的浏览器中使用WOFF 2.0 变体
- 在大多数浏览器中使用WOFF字体
- 在老的安卓(低于4.4)浏览器中使用TTF变体
- 在老的IE(低于IE9)中使用EOT变体
使用压缩来减少字体大小
A font is a collection of glyphs, each of which is a set of paths describing the letter form. The individual glyphs are, of course, different, but they nonetheless contain a lot of similar information that can be compressed with GZIP, or a compatible compressor:
- EOT, 和 TTF 格式默认没有压缩:确保你的服务器使用了GZIP压缩
- WOFF 有内置的压缩 - 确保你的WOFF压缩器使用了最优化的设置。
- WOFF2 使用了用户预定义和压缩算法来减少相对于其他字体格式约30%的文件大小
值得注意的是一些字体格式包括了额外的信息——比如font hitting和kerning信息,这些可能是不必要的,所以可以进行深入优化。
查看你的字体编译器可操作的选项,如果你可以优化上面的内容,进行优化,并在不同的浏览器中查看效果。
Note
- 可以使用 Zopfli compression 来压缩EOT, TTF,和 WOFF 字体格式. Zopfli 是一个 zlib兼容的压缩器,压缩的内容比gzip压缩后还要小约5%。
使用 @font-face定义字体
- 使用
format()定义不同的字体格式
- 使用大量的子集提高性能: 提供大量可选的子集可以让旧的浏览器兼容
- 减少样式化的字体变体来提高页面和文字渲染效果
css语句@font-face定义特殊的字体资源的位置,字体样式等。
字体样式选择
每个 @font-face 声明提供了font family的名字,字体属性的什么和特殊字体来源地址的声明。
@font-face { font-family: ‘Awesome Font‘; font-style: normal; font-weight: 400; src: local(‘Awesome Font‘), url(‘/fonts/awesome.woff2‘) format(‘woff2‘), url(‘/fonts/awesome.woff‘) format(‘woff‘), url(‘/fonts/awesome.ttf‘) format(‘ttf‘), url(‘/fonts/awesome.eot‘) format(‘eot‘); } @font-face { font-family: ‘Awesome Font‘; font-style: italic; font-weight: 400; src: local(‘Awesome Font Italic‘), url(‘/fonts/awesome-i.woff2‘) format(‘woff2‘), url(‘/fonts/awesome-i.woff‘) format(‘woff‘), url(‘/fonts/awesome-i.ttf‘) format(‘ttf‘), url(‘/fonts/awesome-i.eot‘) format(‘eot‘); }
上面一个 Awesome Font 字体的两个表现形式类型:正常和斜体,每个都提供了一系列的不同字体资源设置。
每个src描述符包括了一个按照优先顺序排序的,用逗号进行分隔的资源列表:
local()
表明我们可以使用哪些本地的字体-
url()
指定我们加载哪些外部字体,允许我们使用format()来指代字体的格式
Note
- 除非你指向系统的默认资源,除非很少有本地的字体资源,特别是对手机设备而言,不可能在内部安装了额外的字体。所以,你总是需要提供一个额外的字体位置清单。
浏览器按照顺序执行,上例浏览器执行如下:
- 浏览器显示页面布局并决定哪个字体变形被需要来渲染指定的字体;
- 依次检查local()中的字体是否存在并可使用; 如果本地不存在,依次检查外部字体定义:
- 如果字体格式存在,在初始化和下载字体之间检查浏览器是否支持,如果不支持,进入下一个外部字体选项检查
- 如果字体格式不存在,浏览器下载资源
Note
- 外部字体加载的顺序十分重要,因为浏览器是按照这个顺序依次执行的。
Unicode-range 子集
我们可以将一个大的字体设置成小的子集(例如拉丁,希腊和斯拉夫字母等),并且只是加载这个子集来渲染页面上的字体
这个 unicode-range descriptor 允许我们指定一个逗号隔开的数据集合, 数据可以是下面三种形式中的任何一种:
- Single codepoint (e.g. U+416)
- Interval range (e.g. U+400-4ff): indicates the start and end codepoints of a range
- Wildcard range (e.g. U+4??): ‘?’ characters indicate any hexadecimal digit
例如我们可以将Awesome Font设置成拉丁和日本语的子集格式,它们只有在浏览器需要的时候才会加载
@font-face { font-family: ‘Awesome Font‘; font-style: normal; font-weight: 400; src: local(‘Awesome Font‘), url(‘/fonts/awesome-l.woff2‘) format(‘woff2‘), url(‘/fonts/awesome-l.woff‘) format(‘woff‘), url(‘/fonts/awesome-l.ttf‘) format(‘ttf‘), url(‘/fonts/awesome-l.eot‘) format(‘eot‘); unicode-range: U+000-5FF; /* Latin glyphs */ } @font-face { font-family: ‘Awesome Font‘; font-style: normal; font-weight: 400; src: local(‘Awesome Font‘), url(‘/fonts/awesome-jp.woff2‘) format(‘woff2‘), url(‘/fonts/awesome-jp.woff‘) format(‘woff‘), url(‘/fonts/awesome-jp.ttf‘) format(‘ttf‘), url(‘/fonts/awesome-jp.eot‘) format(‘eot‘); unicode-range: U+3000-9FFF, U+ff??; /* Japanese glyphs */ }
Note
- Unicode-range subsetting is particularly important for Asian languages, where the number of glyphs is much larger than in western languages and a typical ‘full‘ font is often measured in megabytes, instead of tens of kilobytes!
The use of unicode range subsets, and separate files for each stylistic variant of the font allows us to define a composite font family that is both faster and more efficient to download - the visitor will only download the variants and subsets it needs, and they are not forced to download subsets that they may never see or use on the page.
That said, there is one small gotcha with unicode-range: not all browser support it, yet. Some browsers simply ignore the unicode-range hint and will download all variants, while others may not process the @font-face declaration at all. To address this, we need to fallback to “manual subsetting” for older browsers.
Because old browsers are not smart enough to select just the necessary subsets and cannot construct a composite font, we have to fallback to providing a single font resource that contains all necessary subsets, and hide the rest from the browser. For example, if the page is only using Latin characters, then we can strip other glyphs and serve that particular subset as a standalone resource.
- How do we determine which subsets are needed?
- If unicode-range subsetting is supported by the browser, then it will automatically select the right subset. The page just needs to provide the subset files and specify appropriate unicode-ranges in the @font-face rules.
- If unicode-range is not supported then the page needs to hide all unnecessary subsets - i.e. the developer must specify required subsets.
- How do we generate font subsets?
- Use the open-source pyftsubset tool to subset and optimize your fonts.
- Some font services allow manual subsetting via custom query parameters, which you can use to manually specify the required subset for your page - consult the documentation of your font provider.
Font selection and synthesis
Each font family is composed of multiple stylistic variants (regular, bold, italic) and multiple weights for each style, each of which, in turn, may contain very different glyph shapes - e.g. different spacing, sizing, or a different shape altogether.
For example, the above diagram illustrates a font family that offers three different bold weights: 400 (regular), 700 (bold), and 900 (extra bold). All other in-between variants (indicated in gray) are automatically mapped to the closest variant by the browser.
When a weight is specified for which no face exists, a face with a nearby weight is used. In general, bold weights map to faces with heavier weights and light weights map to faces with lighter weights.
Similar logic applies to italic variants. The font designer controls which variants they will produce, and we control which variants we will use on the page - since each variant is a separate download, it’s a good idea to keep the number of variants small! For example, we can define two bold variants for our Awesome Fontfamily:
@font-face {
font-family: ‘Awesome Font‘;
font-style: normal;
font-weight: 400;
src: local(‘Awesome Font‘),
url(‘/fonts/awesome-l.woff2‘) format(‘woff2‘),
url(‘/fonts/awesome-l.woff‘) format(‘woff‘),
url(‘/fonts/awesome-l.ttf‘) format(‘ttf‘),
url(‘/fonts/awesome-l.eot‘) format(‘eot‘);
unicode-range: U+000-5FF; /* Latin glyphs */
}
@font-face {
font-family: ‘Awesome Font‘;
font-style: normal;
font-weight: 700;
src: local(‘Awesome Font‘),
url(‘/fonts/awesome-l-700.woff2‘) format(‘woff2‘),
url(‘/fonts/awesome-l-700.woff‘) format(‘woff‘),
url(‘/fonts/awesome-l-700.ttf‘) format(‘ttf‘),
url(‘/fonts/awesome-l-700.eot‘) format(‘eot‘);
unicode-range: U+000-5FF; /* Latin glyphs */
}
The above example declares the Awesome Font family that is composed of two resources that cover the same set of Latin glyphs (U+000-5FF) but offer two different “weights”: normal (400), and bold (700). However, what happens if one of our CSS rules specifies a different font weight, or sets the font-style property to italic?
- If an exact font match is not available the browser will substitute the closest match.
- If no stylistic match is found (e.g. we did not declare any italic variants in example above), then the browser will synthesize its own font variant.
Authors should also be aware that synthesized approaches may not be suitable for scripts like Cyrillic, where italic forms are very different in shape. It is always better to use an actual italic font rather than rely on a synthetic version.
The example above illustrates the difference between the actual vs. synthesized font results for Open-Sans - all synthesized variants are generated from a single 400-weight font. As you can tell, there is a noticeable difference in the results. The details of how to generate the bold and oblique variants are not specified. Hence, the results will vary from browser to browser, and will also be highly dependent on the font.
Note
- For best consistency and visual results you should not rely on font synthesis. Instead, minimize the number of used font variants and specify their locations, such that the browser can download them when they are used on the page. That said, in some cases a synthesized variant may be a viable option - use with caution.
Optimizing loading and rendering
TL;DR
- Font requests are delayed until the render tree is constructed, which can result in delayed text rendering
- Font Loading API allows us to implement custom font loading and rendering strategies that override default lazyload font loading
- Font inlining allows us to override default lazyload font loading in older browsers
A “full” webfont that includes all stylistic variants, which we may not need, plus all the glyphs, which may go unused, can easily result in a multi-megabyte download. To address this, the @font-face CSS rule is specifically designed to allow us to split the font family into a collection of resources: unicode subsets, distinct style variants, and so on.
Given these declarations the browser figures out the required subsets and variants and downloads the minimal set required to render the text. This behavior is very convenient, but if we’re not careful, it can also create a performance bottleneck in the critical rendering path and delay text rendering - something that we would certainly like to avoid!
Webfonts and the Critical Rendering Path
Lazy loading of fonts carries an important hidden implication that may delay text rendering: the browser must construct the render tree, which is dependent on the DOM and CSSOM trees, before it will know which font resources it will need to render the text. As a result, font requests are delayed well after other critical resources, and the browser may be blocked from rendering text until the resource is fetched.
- Browser requests HTML document
- Browser begins parsing HTML response and constructing the DOM
- Browser discovers CSS, JS and other resources and dispatches requests
- Browser constructs the CSSOM once all CSS content is received and combines it with the DOM tree to construct the render tree
- Font requests are dispatched once render tree indicates which font variants are needed to render the specified text on the page
- Browser performs layout and paints content to the screen
- If the font is not yet available the browser may not render any text pixels
- Once the font is available the browser paints text pixels
The “race” between the first paint of page content, which can be done shortly after the render tree is built, and the request for the font resource is what creates the “blank text problem” where the browser may render page layout but omits any text. The actual behavior differs between various browsers:
- Safari hold text rendering until the font download is complete.
- Chrome and Firefox hold font rendering for up to 3 seconds, after which they use a fallback font, and once the font download has finished they re-render the text once more with the downloaded font.
- IE immediately renders with the fallback font if the request font is not yet available, and re-renders it once the font download is complete.
There are good arguments for and against the different rendering strategies: some people find re-rendering jarring while others prefer to see immediate results and don’t mind the page reflow once the font download has finished - we won’t get into this argument here. The important point is that lazyloading reduces the number of bytes, but also has the potential to delay text rendering. Next, let’s take a look at how we can optimize this behavior.
Optimizing font rendering with the Font Loading API
Font Loading API provides a scripting interface to define and manipulate CSS font faces, track their download progress, and override their default lazyload behavior. For example, if we’re certain that a particular font variant will be required, we can define it and tell the browser to initiate an immediate fetch of the font resource:
var font = new FontFace("Awesome Font", "url(/fonts/awesome.woff2)", {
style: ‘normal‘, unicodeRange: ‘U+000-5FF‘, weight: ‘400‘
});
font.load(); // don‘t wait for render tree, initiate immediate fetch!
font.ready().then(function() {
// apply the font (which may rerender text and cause a page reflow)
// once the font has finished downloading
document.fonts.add(font);
document.body.style.fontFamily = "Awesome Font, serif";
// OR... by default content is hidden, and rendered once font is available
var content = document.getElementById("content");
content.style.visibility = "visible";
// OR... apply own render strategy here...
});
Further, because we can check font status (via check()) method and track its download progress, we can also define a custom strategy for rendering text on our pages:
- We can hold all text rendering until the font is available.
- We can implement a custom timeout for each font.
- We can use the fallback font to unblock rendering and inject a new style that uses desired font once the font is available.
Best of all, we can also mix and match above strategies for different content on the page - e.g. hold text rendering on some sections until font is available, use a fallback and then rerender once the font download has finished, specify different timeouts, and so on.
Note
- Font Loading API is still under development in some browsers. Consider using theFontLoader polyfill, or the webfontloader library, to deliver similar functionality, albeit with the overhead of an additional JavaScript dependency.
Optimizing font rendering with inlining
A simple alternative strategy to using the Font Loading API to eliminate the “blank text problem” is to inline the font contents into a CSS stylesheet:
- CSS stylesheets with matching media queries are automatically downloaded by the browser with high priority as they are required to construct the CSSOM.
- Inlining the font data into CSS stylesheet forces the browser to download the font with high priority and without waiting for the render tree - i.e. this acts as a manual override to the default lazyload behavior.
The inlining strategy is not as flexible and does not allow us to define custom timeouts or rendering strategies for different content, but it is a simple and robust solution that works across all browsers. For best results, separate inlined fonts into standalone stylesheet and serve them with a long max-age - this way, when you update your CSS you are not forcing your visitors to redownload the fonts.
Note
- Use inlining selectively! Recall that the reason @font-face uses lazyload behavior is to avoid downloading unnecessary font variants and subsets. Also, increasing the size of your CSS via aggressive inlining will negatively impact your critical rendering path - the browser must download all CSS before it can construct the CSSOM, build the render tree, and render page contents to the screen.
Optimizing font reuse with HTTP Caching
Font resources are, typically, static resources that don’t see frequent updates. As a result, they are ideally suited for a long max-age expiry - ensure that you specify both a conditional ETag header, and an optimal Cache-Control policy for all font resources.
There is no need to store fonts in localStorage or via other mechanisms - each of those has their set of performance gotchas. The browser’s HTTP cache, in combination with Font Loading API or the webfontloader library, provides the best and most robust mechanism to deliver font resources to the browser.
Optimization checklist
Contrary to popular belief, use of webfonts does not need to delay page rendering or have negative impact on other performance metrics. Well optimized use of fonts can deliver a much better overall user experience: great branding, improved readability, usability, and searchability, all the while delivering a scalable multi-resolution solution that adapts well to all screen formats and resolutions. Don’t be afraid to use webfonts!
That said, a naive implementation may incur large downloads and unnecessary delays. This is where we need to dust off our optimization toolkit and assist the browser by optimizing the font assets themselves and how they are fetched and used on our pages.
- Audit and monitor your font use: do not use too many fonts on your pages, and for each font, minimize the number of used variants. This will assist in delivering a more consistent and a faster experience for your users.
- Subset your font resources: many fonts can be subset, or split into multiple unicode-ranges to deliver just the glyphs required by a particular page - this reduces the filesize and improves download speed of the resource. However, when defining the subsets be careful to optimize for font re-use - e.g. you don’t want to download a different but overlapping set of characters on each page. A good practice is to subset based on script - e.g. Latin, Cyrillic, and so on.
- Deliver optimized font formats to each browser: each font should be provided in WOFF2, WOFF, EOT, and TTF formats. Make sure to apply GZIP compression to EOT and TTF formats, as they are not compressed by default.
- Specify revalidation and optimal caching policies: fonts are static resources that are infrequently updated. Make sure that your servers provide a long-lived max-age timestamp, and a revalidation token, to allow for efficient font re-use between different pages.
- Use Font Loading API to optimize the Critical Rendering Path: default lazyloading behavior may result in delayed text rendering. Font Loading API allows us to override this behavior for particular fonts, and to specify custom rendering and timeout strategies for different content on the page. For older browsers that do not support the API, you can use the webfontloader JavaScript library or use the CSS inlining strategy.