对中文字体进行压缩

2021-06-19coding

引言

中文字体在使用过程中有着不便之处,因为中文字体较多,中文字体包也一般偏大,3、4MB大小很正常,直接引入字体包会减慢用户的加载速度,并造成不必要的资源浪费。因此,对中文字体进行压缩是很有必要的事情。

一些解决方案

Web fonts

谷歌字体等字体提供商可以提供部分中文字体的解决方案。首先,它们有着庞大的CDN网络,在传输过程中可以使用gzip等压缩方案,能够让世界各地的人以较快的速度加载字体。其次,它们采用多种优化手段,比如按照使用频率来分成不同字体包来减小加载体积。 @Google is using #MachineLearning to optimize Chinese, Japanese & Korean web fonts - so they load faster than ever: http://goo.gl/4JsCsz

02/05/2019 from https://design.google/news/google-fonts-launches-chinese-support/

Google Fonts launches Simplified and Traditional Chinese support

New year—new, faster fonts. In the spirit of the Lunar New Year, the Google Fonts catalog now includes five Simplified and two Traditional Chinese fonts—the Chinese written language differs according to country—for designers and developers working with Chinese text. Since Chinese fonts often contain more than 10,000 characters, single font file delivery is unacceptably slow. Building on earlier launches for Korean and Japanese, Google Fonts has analyzed character usage over millions of public web pages to build optimized font ”slicing” patterns for both Simplified and Traditional Chinese. This allows modern web browsers to only download the portions of a font—typically a very small fraction of the complete set—containing the characters that they need.

Head over to Google Fonts to check out—and try out—the Simplified Chinese and Traditional Chinese libraries.

Google字体中引入字体的示例CSS

@font-face {
  font-family: 'Noto Sans SC';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosanssc/v12/k3kXo84MPvpLmixcA63oeALhLOCT-xWNm8Hqd37g1OkDRZe7lR4sg1IzSy-MNbE9VH8V.4.woff2) format('woff2');
  unicode-range: U+1f1e9-1f1f5, U+1f1f7-1f1ff, U+1f21a, U+1f232, U+1f234-1f237, U+1f250-1f251, U+1f300, U+1f302-1f308, U+1f30a-1f311, U+1f315, U+1f319-1f320, U+1f324, U+1f327, U+1f32a, U+1f32c-1f32d, U+1f330-1f357, U+1f359-1f37e;
}
/* [5] */
@font-face {
  font-family: 'Noto Sans SC';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosanssc/v12/k3kXo84MPvpLmixcA63oeALhLOCT-xWNm8Hqd37g1OkDRZe7lR4sg1IzSy-MNbE9VH8V.5.woff2) format('woff2');
  unicode-range: U+fee3, U+fef3, U+ff03-ff04, U+ff07, U+ff0a, U+ff17-ff19, U+ff1c-ff1d, U+ff20-ff3a, U+ff3c, U+ff3e-ff5b, U+ff5d, U+ff61-ff65, U+ff67-ff6a, U+ff6c, U+ff6f-ff78, U+ff7a-ff7d, U+ff80-ff84, U+ff86, U+ff89-ff8e, U+ff92, U+ff97-ff9b, U+ff9d-ff9f, U+ffe0-ffe4, U+ffe6, U+ffe9, U+ffeb, U+ffed, U+fffc, U+1f004, U+1f170-1f171, U+1f192-1f195, U+1f198-1f19a, U+1f1e6-1f1e8;
}
...

对于这种优化,我个人理解如下: 按照一定的粒度,将字体分成多个文件,比如一个4MB的字体包分成100个40KB的字体包。通过机器学习等方法,将一些字频较高的字体、容易同时出现的字体(词语、成语、诗句等)分别打包进同一个字体包,并通过css中unicode-range来给不同文字加载不同的字体包资源。这样的话,一般网页中使用到的中文也只是一部分字体,只需要加载多个资源包就能完全覆盖。同时,就算网页中有很多生僻字,需要付出的代价也只是多加载几个资源包。

image-20210621224958862

font-spider(字蛛)

字蛛是一个智能 WebFont 压缩工具,它能自动分析出页面使用的 WebFont 并进行按需压缩。它主要作用于html文件和css文件,通过检查页面中不同CSS类使用的字体来进行压缩。
它可以满足一些简单的需求,但在使用中有着较多不便之处。

fontmin

fontmin是字蛛实际使用压缩字体的库。可以从字体包中提取指定的字体,并生成压缩的字体包,同时支持转换为eot、woff2、woff、ttf等格式。本文也通过fontmin来进行简单的字体压缩操作。

对文件中使用到的中文字体压缩

从指定文件读取中文字体

  • 匹配文件,可以使用glob来进行文件的匹配

    // 遍历src目录下的全部ts和tsx文件
    glob("src/**/*.@{ts|tsx}", (err, files) {
    	if (err) throw err
    	console.log(files)
    	// do something...
    })
  • 匹配中文字体

    • 中文字符正则匹配:/[\u4e00-\u9fa5]//\p{sc=Han}/u (unicode正则匹配)
    • 中文标点符号正则匹配:/[\u3000-\u301e\ufe10-\ufe19\ufe30-\ufe44\ufe50-\ufe6b\uff01-\uffee]/

整体流程为,以字符串形式读取匹配到的文件,并通过正则匹配文件中使用到的中文字体(注释的中文字体也会包括在内),从而得到文件中使用的中文字符的集合。再加上额外的一些英文符号,如abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,./;’[]\`-=<>?:”{}|~!@#$%^&*()_+等符号。

使用fontmin压缩中文字体

const text = "读取文件中使用到的全部字符"; // 压缩这里的全部中文字符
const fontData = [
  {
    fontLoc: 'src/resource/HYWenHei-85W.ttf',
    fontFamily: 'HYWenHei-85W',
  },
  {
    fontLoc: 'src/resource/HYWenHei-65W.ttf',
    fontFamily: 'HYWenHei-65W',
  },
];
const outputDir = 'src/font'; // 输出文件位置
fontData.map(({ fontLoc, fontFamily }) => {
      return new Promise((resolve, reject) => {
        const fontmin = new Fontmin();
        const { name } = path.parse(fontLoc);
        fontmin.src(fontLoc);
        fontmin.use(
          Fontmin.glyph({
            text,
            hinting: false,
          }),
        );
        fontmin.use(Fontmin.ttf2eot());
        fontmin.use(Fontmin.ttf2woff());
        fontmin.use(Fontmin.ttf2woff2());
        fontmin.use(Fontmin.ttf2svg());
        fontmin.dest(outputDir);
        fontmin.run(function (err, files) {
          if (err) {
            reject(err);
          }
          resolve(`
@font-face {
  font-family: ${fontFamily};
  src: url('./${name}.eot');
  src:
    url('./${name}.eot') format('embedded-opentype'),
    url('./${name}.woff2') format('woff2'),
    url('./${name}.woff') format('woff'),
    url('./${name}.ttf') format('truetype'),
    url('./${name}.svg') format('svg');
  font-weight: normal;
  font-style: normal;
}
`);
        });
      });
    }),
  ).then((values) => {
    fs.writeFileSync(
      path.resolve(outputDir, 'font.css'),
      `/* generated by srcipts/compressFont.js */
${values.join('\n')}
`,
);

在输出目录下生成的CSS文件如下,同时输出目录中也会生成压缩过的字体包的多种格式文件:

/* generated by srcipts/compressFont.js */
@font-face {
  font-family: HYWenHei-85W;
  src: url('./HYWenHei-85W.eot');
  src:
    url('./HYWenHei-85W.eot') format('embedded-opentype'),
    url('./HYWenHei-85W.woff2') format('woff2'),
    url('./HYWenHei-85W.woff') format('woff'),
    url('./HYWenHei-85W.ttf') format('truetype'),
    url('./HYWenHei-85W.svg') format('svg');
  font-weight: normal;
  font-style: normal;
}

@font-face {
  font-family: HYWenHei-65W;
  src: url('./HYWenHei-65W.eot');
  src:
    url('./HYWenHei-65W.eot') format('embedded-opentype'),
    url('./HYWenHei-65W.woff2') format('woff2'),
    url('./HYWenHei-65W.woff') format('woff'),
    url('./HYWenHei-65W.ttf') format('truetype'),
    url('./HYWenHei-65W.svg') format('svg');
  font-weight: normal;
  font-style: normal;
}

直接在工程文件中引入该CSS文件即可。

使用范围

中文字符压缩主要应用场景还是静态网站,使用的中文字体都在文件中引入,没有动态生成的中文字符。可以通过scripts/compressFont.js文件去实际运行字符压缩。记得在yarn startyarn build命令前加入node scripts/compressFont.js命令。

不足之处

  • 在默认情况下,引用的js代码库则不会扫描进去,可能会造成一些字体格式显示不正确,可以通过手动fix加入这些字体来进行修正。
  • 随着代码文件的增大,使用过的中文字符增加,字体包也会逐渐增大。同时,注释中使用过的中文字符也会被扫描打包。(对于需要国际化的网站,在一般情况下,中文字体都在独立的目录下。直接扫描此目录将使用过的中文字符进行压缩能够起到不错的效果)

展望

  • 在构建工具中实现插件,可以忽略掉注释中使用的中文字符。
  • 在构建完成后对build目录进行中文字符扫描,并将生成的css文件引入到构建完成的html文件中,这样可以允许代码库中使用的字符也被扫描到。不过应注意build构建的文件中字符编码的问题:以create-react-app为例,在build生成的js文件中,中文字符以unicode的形式存在,如”\u6587\u4ef6\u7c7b\u578b\u9519\u8bef\uff0c\u8bf7\u4e0a\u4f20xlsx\u6587\u4ef6”,因此需要先通过正则匹配unicode,再将unicode转为字符并用正则匹配判断是否中文。

使用案例

  • 以此网站为例。原字体包大小为3199KB,进行压缩后,在PC客户端网页实际访问的字体包为woff2格式,目前大小仅有114KB,如果开启gzip压缩,实际传输的流量消耗将会更低。
The end