便宜vps推荐
搬瓦工优惠|主机测评网!

网页字体Font压缩篇:终极优化方案——中文字体woff2分片加载

今天分享web网页中文字体终极压缩方案,也就是切割分片加载(将一个大的字体包压缩切割成若干个小的字体文件)。很多大厂都在使用这个加载方案:把字体文件分片后放到CDN上加速,达到无卡顿加载较大的字体文件。因此,对中文字体进行压缩是很有必要的事情。

Web fonts

https://fonts.google.com/

谷歌字体等字体提供商可以提供部分中文字体的解决方案。首先,它们有着庞大的CDN网络,在传输过程中可以使用gzip等压缩方案,能够让世界各地的人以较快的速度加载字体。其次,它们采用多种优化手段,比如按照使用频率来分成不同字体包来减小加载体积。

google字体引入示例

@font-face {
  font-family: 'Noto Sans SC';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts-gstatic.lug.ustc.edu.cn/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.lug.ustc.edu.cn/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来给不同文字加载不同的字体包资源。这样的话,一般网页中使用到的中文也只是一部分字体,只需要加载多个资源包就能完全覆盖。同时,就算网页中有很多生僻字,需要付出的代价也只是多加载几个资源包。

font-spider(字蛛)

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

fontmin

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

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

从指定文件读取中文字体

  • 匹配文件,可以使用glob来进行文件的匹配
    // 遍历src目录下的全部ts和tsx文件
    const filePaths = glob("src/**/*.@{ts|tsx}");
  • 匹配中文字体
    • 中文字符正则匹配:/[\u4e00-\u9fa5]/ 或 /\p{sc=Han}/u (unicode正则匹配)
    • 中文标点符号正则匹配:/[\u3000-\u301e\ufe10-\ufe19\ufe30-\ufe44\ufe50-\ufe6b\uff01-\uffee]/

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

使用fontmin压缩中文字体

本网站使用的代码参考:

/**
 * 遍历某个文件夹里的文件的中文字体,生成压缩后的各个类型的字体文件
 */
// 使用的 v10 glob,如果报错可以检查下版本
const { glob } = require("glob");
const fs = require("fs");
const Fontmin = require("fontmin");
const path = require("path");

const outputDir = "src/fonts"; // 输出文件位置

const matchChinese =
  /[\u4e00-\u9fa5\u3000-\u301e\ufe10-\ufe19\ufe30-\ufe44\ufe50-\ufe6b\uff01-\uffee]/gmu;

// 不同字体对应的扫描文件,匹配方法
const fontData = [
  {
    files: ["content/**/*.@(md|mdx)", "src/**/*.@(ts|tsx)"],
    fontPath: path.resolve(__dirname, "../src/assets/HYWenHei-55W.ttf"),
    fontFamily: "HYWenHei",
    defaultText: `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,./;'[]\\\`-=<>?:\"{}|~!@#$%^&*()_+`,
    match(str) {
      return (str.match(matchChinese) || []).join("");
    },
  },
  {
    files: [
      "content/blog/rhymes/*.@(md|mdx)",
      "content/blog/secrets/poems.mdx",
    ],
    fontPath: path.resolve(__dirname, "../src/assets/AaKaiSong.ttf"),
    fontFamily: "AaKaiSong",
    defaultText: `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,./;'[]\\\`-=<>?:\"{}|~!@#$%^&*()_+`,
    match(str) {
      return (str.match(matchChinese) || []).join("");
    },
  },
  {
    files: [],
    fontPath: path.resolve(__dirname, "../src/assets/SourceCodePro-Medium.ttf"),
    fontFamily: "SourceCodePro",
    defaultText: `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,./;'[]\\\`-=<>?:\"{}|~!@#$%^&*()_+`,
  },
];

const trimText = (text = "") => {
  const cache = Object.create(null);
  const arr = [];
  text.split("").forEach((char) => {
    if (!(char in cache)) {
      arr.push(char);
      cache[char] = true;
    }
  });
  return arr.join("");
};

const promiseList = fontData.map(async (item) => {
  const { files, fontPath, fontFamily, defaultText, match } = item;

  const filePaths = await glob(files);

  const matchTextArr = filePaths.map((file) => {
    const data = fs.readFileSync(file, "utf-8");

    const matchText = match(data);

    console.log(`读取${file}完成`);

    return matchText;
  });

  const textSubset = trimText(matchTextArr.join("") + defaultText);

  const fontmin = new Fontmin();

  const { name } = path.parse(fontPath);

  fontmin.src(fontPath);
  fontmin.use(
    Fontmin.glyph({
      text: textSubset,
      hinting: false,
    })
  );
  fontmin.use(Fontmin.ttf2woff());
  fontmin.use(Fontmin.ttf2woff2());
  fontmin.dest(outputDir);

  await new Promise((resolve, reject) => {
    fontmin.run(function (err, files) {
      if (err) {
        reject(err);
      }
      console.log(`写入字体${name}成功`);
      resolve();
    });
  });

  return `@font-face {
  font-family: ${fontFamily};
  src: url("./${name}.woff2") format('woff2'),
    url("./${name}.woff") format('woff'),
    url("./${name}.ttf") format('truetype');
  font-weight: normal;
  font-style: normal;
}
`;
});

Promise.all(promiseList).then((values) => {
  fs.writeFileSync(
    path.resolve(outputDir, "font.css"),
    `/* generated by srcipts/compressFont.js */
${values.join("")}
`
  );
});

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

/* generated by srcipts/compressFont.js */
@font-face {
  font-family: HYWenHei;
  src: url("./HYWenHei-55W.woff2") format('woff2'),
    url("./HYWenHei-55W.woff") format('woff'),
    url("./HYWenHei-55W.ttf") format('truetype');
  font-weight: normal;
  font-style: normal;
}
@font-face {
  font-family: AaKaiSong;
  src: url("./AaKaiSong.woff2") format('woff2'),
    url("./AaKaiSong.woff") format('woff'),
    url("./AaKaiSong.ttf") format('truetype');
  font-weight: normal;
  font-style: normal;
}
@font-face {
  font-family: SourceCodePro;
  src: url("./SourceCodePro-Medium.woff2") format('woff2'),
    url("./SourceCodePro-Medium.woff") format('woff'),
    url("./SourceCodePro-Medium.ttf") format('truetype');
  font-weight: normal;
  font-style: normal;
}

直接在工程文件中引入该CSS文件即可,一个简单的效果。

AaKaiSong: 会有清亮的风使草木伏地...

HYWenHei: 会有清亮的风使草木伏地…
SourceCodePro: 0123456789

使用范围

中文字符压缩主要应用场景还是静态网站,使用的中文字体都在文件中引入,没有动态生成的中文字符。可以通过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转为字符并用正则匹配判断是否中文。
  • 使用 babel 插件,扫描代码文件,将需要特定字体的语言用特定方法包裹。可以扫描源代码也可以扫描打包后的文件。一个简单的示例如下:
// 什么名字都行
const customText = (strs, ...params) => {
  const result = [];
  for(let i = 0, len = strs.length - 1; i < len; i++) {
    result.push(strs[i], params[i])
  };
  result.push(strs[strs.length - 1]);
  return result.join('');
};
// custom file
const title = customText`示例字体`;

babel 插件处理:

const { parse } = require('@babel/parse');
const traverse = require('@babel/traverse').default;

const ast = parse(code, {
  sourceType: 'module'
});

const words = [];

traverse(ast, {
  /** customFont`需要被扫描的中文文案`*/
  TaggedTemplateExpression(path) {
    const node = path.node;
    if (node.tag.name === 'customFont') {
      const spans = node.quasi.quasis;
      const spansText = spans.map(v => v.value.cooked);
      words.push(spansText.join(''));
    }
  },
})

使用案例

  • 以此网站为例。原字体包大小为3199KB,进行压缩后,在PC客户端网页实际访问的字体包为woff2格式,目前大小仅有114KB260KB

字体切片

以上所说的方法,只适合于静态加载的内容,如果说,需要自定义字体能够展示用户输入的内容,可以采用下面的方法:

使用

github 地址: https://github.com/voderl/font-slice

  1. 安装
npm install --save-dev font-slice
yarn add -D font-slice
  1. 使用
const createFontSlice = require('font-slice');

createFontSlice({
  // fontPath
  fontPath: path.resolve(__dirname, 'YourPath.ttf'),
  // outputDir
  outputDir: path.resolve(__dirname, './output'),
})

可能等待时间较长,请耐心等待,完成后可以直接预览字体。

  1. 引用生成的 font.css 文件,设置对应的 fontFamily 即可

将生成的产物部署到 cdn 上,直接引用 cdn 的地址就可以了。

更多配置项请前往 github 页查看。

注意项

  • 默认的 font-display 为 swap,即在字体没有加载完成时,先使用别的字体展示。需要调整的话可以在传入的 options 里指明。如果设置为 block,当字体没有加载完成时,会在一定的时间里不展示对应的内容。 更多 font-display 介绍请看这里
  • 同时建议在 cdn 中将对应的字体目录直接设置一定时长的浏览器缓存,避免因字体加载导致页面内容闪动。
  • 如果在 canvas 中使用,需要先加载文案对应的字体子集再去渲染
    // 字体引入 css 文件需要先加载完成
    document.fonts.load(`14px ${fontFamily}`, '指定文案').then(() => {
      ctx.fillText('指定文案');
    });

数据

得意黑字体为例为例:

处理前 ttf 大小 2074KB,woff2 大小 928KB.

处理后每个类型的字体生成 95 个文件:
ttf   总大小为 2.3M (最小文件 3.4K,最大文件 55K)
woff2 总大小为 1.3M (最小文件 1.5K,最大文件 33K)

实际加载页面的体积由页面使用的字符决定,以该页面为例,只需要加载 386KB 就能覆盖全部字符。

如果使用上面的像素字体 ark-pixel-font, 只需要加载 133 KB 资源即可覆盖本页面。

原理

以上的效果是怎么做到的呢?

很久之前我就写过一篇中文字体的解决方案,对中文字体进行压缩。

这篇文章主要讲的是扫描你的代码及博文中所用到的汉字,然后提取字体文件的子集,从而达到一个比较小的字体加载体积。

但这样的方法,在面对用户自定义输入、比如评论等行为时就处理不了了。

文章来源于:https://voderl.cn/js/对中文字体进行切片/

赞(0)
未经允许不得转载:雪花测评 » 网页字体Font压缩篇:终极优化方案——中文字体woff2分片加载

推荐使用迅马数据云服务器建站,性价比高:点我进入

登录

找回密码

注册