前端性能优化系列文章之三 - 通过字体优化大幅提升性能 - Node.js 节日系列文章之八

这是来自 Mozilla 身份团队的“Node.JS 节日系列文章”的第 8 篇文章,共 12 篇。今天我们讨论更多关于前端性能优化的话题!

我们使用字体子集技术将 Persona 的字体占用空间减少了 85%,从 300 KB 降至 45 KB。本文详细介绍了我们如何实现这些性能改进,并提供了一些工具供您使用。

介绍 connect-fonts

connect-fonts 是一款 Connect 字体管理中间件,通过提供特定区域的字体子集文件来改善 @font-face 的性能,显著减少下载的字体大小。它还生成特定区域/浏览器的 @font-face CSS,并管理 Firefox 和 IE 9+ 所需的 CORS 标头。字体子集从“字体包”中提供 - 包含字体子集的目录树,以及一个简单的 JSON 配置文件。一些常见的开源字体在预先生成的字体包中可用,您可以在 npm 上找到它们,创建您自己的字体包也很简单。

(感觉迷茫?我们在网上收集了一些关于 @font-face 资源的参考,您可以 参考 这些信息。)

静态与动态字体加载

当您只向所有用户提供一个大型字体时,设置网页字体并不复杂

  • 生成 @font-face CSS 并插入到您的现有 CSS 中
  • 从您的 TTF 或 OTF 文件生成完整的字体系列,然后将它们放置在 web 服务器可以访问的位置
  • 如果字体是从单独的域提供的,请向您的 web 服务器添加 CORS 标头,因为 Firefox 和 IE9+ 对字体实施同源策略

这些步骤相当简单;优秀的 FontSquirrel 生成器 可以为您生成所有缺失的字体文件和 @font-face CSS 声明。您仍然需要查看 Nginx 或 Apache 文档来了解如何添加 CORS 标头,但这并不难。

如果您想利用字体子集技术来大幅提升性能,事情会变得更复杂。您将拥有每个支持区域的字体文件,并且需要动态修改 @font-face CSS 声明以指向正确的 URL。CORS 管理仍然是必要的。这就是 connect-fonts 解决的问题。

字体子集:概述

默认情况下,字体文件包含许多字符:英语使用者熟悉的拉丁字符集;法语和德语等语言添加到拉丁字符集中的重音符号和重音字符;西里尔字母或希腊字母等其他字母表。一些字体还包含许多有趣的符号,尤其是当它们支持 Unicode 时( 有谁认识吗?)。一些字体还支持东亚语言。字体文件包含所有这些内容,以便它们能够胜任地为尽可能多的受众服务。所有这些灵活性都导致了较大的文件大小;微软 Arial Unicode 拥有 Unicode 2.1 中所有语言和符号的字符,其重量惊人地达到了 22 兆字节。

相比之下,典型的网页只需要一个字体来完成一项特定任务:显示网页内容,通常只用一种语言,并且通常不包含奇特的符号。通过将提供的字体文件缩减到我们所需的子集,我们可以节省大量网页重量。

字体子集带来的性能提升

让我们比较一些常用字体和一些区域的本地化字体文件大小与完整文件大小。即使您只提供英文网站,通过提供英文子集也可以节省大量字节。

更小的字体意味着更快的加载时间,以及更短的等待时间,以便在屏幕上显示带格式的文本。如果您想在移动设备上使用 @font-face,这一点尤其重要;如果您的用户恰好使用的是 2G 网络,节省 50 KB 可以将加载时间缩短 2-3 秒。另一个因素:移动设备缓存很小,字体子集更有可能保留在缓存中。

Open Sans 常规字体,完整字体(默认)和一些子集的大小(KB)

Chart comparing file sizes of Open Sans subsets. Full font, 104 KB. Cyrillic, 59 KB. Latin, 29 KB. German, 22 KB. English, 20 KB. French, 24 KB.

相同的字体,经过压缩(KB)

Chart comparing file sizes of Open Sans subsets when gzipped. Full font, 63 KB. Cyrillic, 36 KB. Latin, 19 KB. German, 14 KB. English, 13 KB. French, 15 KB.

即使在压缩后,使用 Open Sans 的英文子集(13 KB),而不是完整字体(63 KB),也可以将字体大小减少 80%。请注意,这仅仅是针对一个字体文件进行的缩减 - 大多数网站使用多个字体文件。潜力巨大!

使用 connect-fonts,Mozilla Persona 的字体占用空间从 300 KB 降至 45 KB,减少了 85%。 这相当于在典型的 3G 连接上节省了几秒钟的下载时间,在典型的 2G 连接上最多可以节省 10 秒。

进一步优化

如果您希望调整每一个字节和 HTTP 请求,可以将 connect-fonts 配置为返回生成的 CSS 字符串,而不是单独的文件。更进一步地说,connect-fonts 默认情况下提供尽可能小的 @font-face 声明,省略给定浏览器不接受的文件类型的声明。

示例:将 connect-fonts 添加到应用程序

假设您有一个非常简单的 express 应用程序,用于提供当前时间

// app.js
const
ejs = require('ejs'),
express = require('express'),
fs = require('fs');

var app = express.createServer(),
  tpl = fs.readFileSync(__dirname, '/tpl.ejs', 'utf8');

app.get('/time', function(req, res) {
  var output = ejs.render(tpl, {
    currentTime: new Date()
  });
  res.send(output);
});

app.listen(8765, '127.0.0.1');

并使用一个非常简单的模板

// tpl.ejs

the time is <%= currentTime %>.

让我们逐步介绍如何添加 connect-fonts 以提供 Open Sans 字体,这是 多个 预制字体包之一。

应用程序更改

  1. 通过 npm 安装

     $ npm install connect-fonts
     $ npm install connect-fonts-opensans
    
  2. 引入中间件

     // app.js - updated to use connect-fonts
     const
     ejs = require('ejs'),
     express = require('express'),
     fs = require('fs'),
     // add requires:
     connect_fonts = require('connect-fonts'),
     opensans = require('connect-fonts-opensans');
    
     var app = express.createServer(),
       tpl = fs.readFileSync(__dirname, '/tpl.ejs', 'utf8');
    
  3. 初始化中间件

      // app.js continued
      // add this app.use call:
      app.use(connect_fonts.setup({
        fonts: [opensans],
        allow_origin: 'http://localhost:8765'
      })
    

    connect_fonts.setup() 的参数包括

    • fonts:要启用的字体数组,
    • allow_origin:我们为其提供字体的来源;connect-fonts 使用此信息为需要它的浏览器设置 Access-Control-Allow-Origin 标头(Firefox 3.5+,IE 9+)
    • ua(可选):列出我们将为其提供字体的用户代理的参数。默认情况下,connect-fonts 使用 UA 嗅探来仅为可以解析字体格式的浏览器提供字体,从而减小 CSS 大小。ua: 'all' 会覆盖此设置,为所有浏览器提供所有字体。

  4. 在您的路由中,将用户的区域传递给模板

     // app.js continued
     app.get('/time', function(req, res) {
       var output = ejs.render(tpl, {
         // pass the user's locale to the template
         userLocale: detectLocale(req),
         currentTime: new Date()
       });
       res.send(output);
     });
    
  5. 检测用户的首选语言。Mozilla Persona 使用 i18n-abidelocale 是另一个不错的选择;两者都可以通过 npm 获得。为了使示例简短,我们只获取 Accept-Language 标头 的前两个字符

     // oversimplified locale detection
     function detectLocale(req) {
       return req.headers['accept-language'].slice(0,2);
     }
    
     app.listen(8765, '127.0.0.1');
     // end of app.js
    

模板更改

现在我们需要更新模板。connect-fonts 假设路由的格式为

    /:locale/:font-list/fonts.css

例如,

    /fr/opensans-regular,opensans-italics/fonts.css

在本例中,我们需要

  1. 向模板添加一个样式表 <link>,使其与 connect-fonts 预期的路由匹配

     // tpl.ejs - updated to use connect-fonts
     
     
    
  2. 更新页面样式以使用新字体,我们就完成了!

     // tpl.ejs continued
     
     

    the time is <%= currentTime %>.

connect-fonts 生成的 CSS 基于用户的区域和浏览器。以下是 Open Sans 的“en”本地化子集的示例

/* this is the output with the middleware's ua param set to 'all' */
@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: 400;
  src: url('/fonts/en/opensans-regular.eot');
  src: local('Open Sans'),
       local('OpenSans'),
       url('/fonts/en/opensans-regular.eot#') format('embedded-opentype'),
       url('/fonts/en/opensans-regular.woff') format('woff'),
       url('/fonts/en/opensans-regular.ttf') format('truetype'),
       url('/fonts/en/opensans-regular.svg#Open Sans') format('svg');
}

如果您不想承担额外的 HTTP 请求成本,可以使用 connect_fonts.generate_css() 方法将此 CSS 返回为字符串,然后将其作为构建/压缩过程的一部分插入到您的现有 CSS 文件中。

就这样!我们的应用程序提供了一个时尚的时间戳。如果您想试用该示例代码,可以在 githubnpm 上找到。

我们已经介绍了预制字体包,但是创建您自己的付费字体包也很简单。您可以在 connect-fontsreadme 上找到说明。

总结

字体子集可以为使用网页字体的网站带来巨大的性能提升;如果您在国际化的 Connect 应用程序中自托管字体,connect-fonts 会处理很多复杂性。如果您的网站尚未国际化,您仍然可以使用 connect-fonts 来提供您的本地子集,它仍然会为您生成 @font-face CSS 和任何必要的 CORS 标头,此外您还可以获得一个流畅的升级路径,以便以后进行国际化。

未来的方向

目前,connect-fonts 基于区域处理子集。如果它还能为不需要 hinting 的平台(除 Windows 之外的所有平台)去除字体 hinting 会怎么样?如果它还能选择性地压缩字体并添加未来缓存标头会怎么样?还有很多有趣的工作要做。如果您想贡献想法或代码,我们非常欢迎!获取 源代码 并参与进来。

系列文章中的先前文章

这是一个包含 12 篇关于 Node.js 的系列文章的第 8 篇文章。之前的文章是

关于 Jared Hirsch

@6a68 热衷于 Persona,擅长弹奏象牙琴,鞋子总是沾满了沙子。

Jared Hirsch 的更多文章……

关于 Robert Nyman [荣誉编辑]

Mozilla Hacks 的技术布道者和编辑。发表关于 HTML5、JavaScript 和开放网络的演讲和博文。Robert 是 HTML5 和开放网络的坚定支持者,自 1999 年以来一直致力于 Web 前端开发,在瑞典和纽约市工作。他还在 http://robertnyman.com 上定期发表博文,热爱旅行和结识新朋友。

Robert Nyman [荣誉编辑] 的更多文章……


4 条评论

  1. Chris Lilley

    您是否对字距表进行子集化处理?

    这是否还会删除 GSUB 可能使用的字形?

    最后,这假设用户只说一种语言,并且从不阅读任何不在他们“区域设置”范围内覆盖的语言的任何内容(即使是短语)?

    2013 年 3 月 19 日 下午 7:02

    1. Jared Hirsch

      嗨,Chris,

      Connect-fonts 不会生成子集,而是提供预子集化的字体。

      因此,您可以根据需要自定义字体,然后将这些子集变成一个 *字体包*,connect-fonts 可以提供。操作很简单;github 上的 connect-fonts 项目自述文件中提供了说明。

      字体包基本上是一堆包含不同子集的目录,加上一个 JSON 配置文件,用于将 Accept-Language 首选项映射到子集。Connect-fonts 智能地按照 Accept-Language 标头中的语言列表进行处理,并提供最佳的匹配项。

      至于现有的字体包,它们是使用 FontSquirrel 生成器中的语言子集化功能生成的,因此您需要查看该生成器的具体信息。

      2013 年 3 月 20 日 上午 7:21

  2. Dan Milon

    很棒的帖子!

    假设您没有这样做,而是提供非子集化的字体,但也有远期缓存标题。这难道还不够吗?我认为,这种优化只涵盖了冷启动阶段。我是否遗漏了什么?

    2013 年 3 月 20 日 下午 12:12

    1. Jared Hirsch

      嗨,Dan!

      是的,这里带来的收益适用于缓存未初始化的用户。

      但是,移动用户拥有更小的缓存和更慢、可能是计量的网络连接,因此可以说未初始化缓存对他们来说是一个更重要的因素。(收集一些数据来验证该假设将非常有用。)

      在缓存已初始化的情况下,一个有趣的问题是,是否拥有更小的字体文件会缩短 FOUT 时间,仅仅是因为浏览器需要解析的更少。我猜想这只是一个次要的影响,但是,再次强调,在不同浏览器中花一些时间调查这个问题将非常有用。

      2013 年 3 月 20 日 下午 3:14

本文的评论已关闭。