什么是字体分块?
字体分块(Font Chunking / Subsetting by Range)是一种针对网页字体(Web Font)的性能优化技术。它的核心思想是将一个庞大的完整字体文件,根据字符的 Unicode 范围或其他标准(如使用频率、语言等),预先分割成多个较小的字体片段(Chunks)。然后,利用 CSS 的 @font-face
规则和 unicode-range
描述符,让浏览器只下载当前页面实际需要渲染的字符所在的那个(或那些)字体片段,而不是一次性下载整个庞大的字体文件。
这种技术对于包含大量字形(Glyphs)的字体尤其有效,例如中文字体(GBK、GB2312、GB18030 覆盖数千甚至数万汉字)、日文、韩文等 CJK 字体,以及一些包含多种语言符号的大型西文字体。
为什么需要字体分块?
- 性能问题: 完整的中文字体文件通常体积巨大,动辄几 MB 甚至几十 MB。在网页加载时,如果需要等待整个字体文件下载完成才能正确渲染文本,会导致页面加载速度变慢,甚至出现字体闪烁(FOIT/FOUT)或长时间白屏,严重影响用户体验。
- 带宽浪费: 一个页面通常只会用到字体库中很少一部分字符。下载整个字体文件意味着用户下载了大量根本用不到的字形数据,造成了不必要的带宽消耗,对移动端用户尤其不友好。
字体分块通过按需加载,有效解决了以上两个问题。
字体分块的具体机制
字体分块的实现主要依赖以下几个步骤和技术:
-
字体分析 (Font Analysis):
- 确定如何划分字体,最常用的是基于 Unicode 字符范围。
- 例子:基本拉丁、常用标点、CJK 符号、常用汉字(可分多级)、生僻字等。
- 自动化工具(如
font-spider
)可分析项目找出实际使用字符生成精确子集,但字体分块更侧重预先按 范围 划分以应对动态内容。
-
字体子集生成 (Subset Generation):
- 使用字体工具(如
pyftsubset
)根据定义的范围,从原始字体生成多个小的字体文件(推荐 WOFF2 格式)。 - 每个小文件只包含其负责范围的字形。
- 使用字体工具(如
-
CSS @font-face 配置:
- 为同一个
font-family
定义多个@font-face
规则。 - 每个规则指向一个分块字体文件 (
src
)。 - 关键: 每个规则使用
unicode-range
描述符声明该字体文件负责的 Unicode 范围。
- 为同一个
-
浏览器按需加载 (Browser On-Demand Loading):
- 核心机制澄清: 浏览器并不会 在解析 CSS 时就立刻下载所有
@font-face
中定义的字体块 URL。unicode-range
的关键作用在于告知浏览器每个字体块负责哪些字符范围。 - 真正的下载发生在页面渲染阶段:
- 当浏览器渲染页面,遇到需要使用该
font-family
的文本时,它会检查文本中的每一个字符的 Unicode 值。 - 浏览器查找所有为该
font-family
定义的@font-face
规则,看哪个规则的unicode-range
覆盖了当前字符的 Unicode 值。 - 关键点: 如果找到了匹配的
@font-face
规则,并且其src
指向的字体块文件尚未被下载 ,浏览器此时才会发起网络请求去下载那个特定的字体块。 - 一旦下载完成,浏览器就使用该字体块中的字形来渲染对应的字符。
- 如果一个字符的范围对应的字体块已经被下载(可能因为页面上之前的某个字符触发了下载),浏览器会直接使用缓存,不会重复下载。
- 如果页面需要多个范围的字符(如英文和中文),浏览器会根据需要分别触发下载对应的多个字体块。
- 当浏览器渲染页面,遇到需要使用该
- 核心机制澄清: 浏览器并不会 在解析 CSS 时就立刻下载所有
完整示例
假设我们有一个名为 MyCustomFont.ttf
的中文字体,体积很大 (15MB)。我们的网站主要内容是中文,但也包含少量英文和数字。我们希望通过字体分块来优化加载。
第 1 步:分析与定义分块策略
我们决定按以下范围划分字体块:
- 基础拉丁与数字 (Basic Latin & Digits):
U+0020-U+007E
(空格、标点、0-9、A-Z、a-z) - 常用中文标点 (Common CJK Punctuation):
U+3000-U+303F
(如:,。?!;:) - 一级常用汉字 (Common Hanzi - Set 1):
U+4E00-U+62FF
(假设这是我们根据分析确定的最高频使用的汉字范围) - 二级常用汉字 (Common Hanzi - Set 2):
U+6300-U+7FFF
- 其他常用汉字 (Other Common Hanzi):
U+8000-U+9FA5
- (可选) 其他字符块...
第 2 步:生成字体子集文件
使用 pyftsubset
工具(需要安装 fonttools
:pip install fonttools
):
bash
# 原始字体文件
SOURCE_FONT="MyCustomFont.ttf"
# 输出目录
OUTPUT_DIR="font-chunks"
mkdir -p $OUTPUT_DIR
# 生成 WOFF2 格式的字体块
pyftsubset $SOURCE_FONT --output-file="$OUTPUT_DIR/myfont-latin.woff2" --unicodes="U+0020-007E" --flavor=woff2 --with-zopfli
pyftsubset $SOURCE_FONT --output-file="$OUTPUT_DIR/myfont-cjk-punct.woff2" --unicodes="U+3000-303F" --flavor=woff2 --with-zopfli
pyftsubset $SOURCE_FONT --output-file="$OUTPUT_DIR/myfont-hanzi-1.woff2" --unicodes="U+4E00-62FF" --flavor=woff2 --with-zopfli
pyftsubset $SOURCE_FONT --output-file="$OUTPUT_DIR/myfont-hanzi-2.woff2" --unicodes="U+6300-7FFF" --flavor=woff2 --with-zopfli
pyftsubset $SOURCE_FONT --output-file="$OUTPUT_DIR/myfont-hanzi-3.woff2" --unicodes="U+8000-9FA5" --flavor=woff2 --with-zopfli
# ... 可以根据需要生成更多块 ...
执行后,font-chunks
目录下会生成多个 .woff2
文件,每个文件都比原始字体小得多。
第 3 步:在 CSS 中配置 @font-face
在你的 CSS 文件 (e.g., style.css
) 中添加如下规则:
css
/* 定义基础拉丁字母和数字块 */
@font-face {
font-family: 'MyCustomFont';
src: url('font-chunks/myfont-latin.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap; /* 推荐设置,改善加载体验 */
unicode-range: U+0020-007E; /* 关键:指定此块负责的 Unicode 范围 */
}
/* 定义中文标点块 */
@font-face {
font-family: 'MyCustomFont';
src: url('font-chunks/myfont-cjk-punct.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
unicode-range: U+3000-303F; /* 关键:指定此块负责的 Unicode 范围 */
}
/* 定义一级常用汉字块 */
@font-face {
font-family: 'MyCustomFont';
src: url('font-chunks/myfont-hanzi-1.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
unicode-range: U+4E00-62FF; /* 关键:指定此块负责的 Unicode 范围 */
}
/* 定义二级常用汉字块 */
@font-face {
font-family: 'MyCustomFont';
src: url('font-chunks/myfont-hanzi-2.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
unicode-range: U+6300-7FFF; /* 关键:指定此块负责的 Unicode 范围 */
}
/* 定义其他常用汉字块 */
@font-face {
font-family: 'MyCustomFont';
src: url('font-chunks/myfont-hanzi-3.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
unicode-range: U+8000-9FA5; /* 关键:指定此块负责的 Unicode 范围 */
}
/* ... 可以根据需要定义更多块 ... */
/* 在需要的地方使用字体 */
body {
font-family: 'MyCustomFont', sans-serif;
}
h1 {
font-family: 'MyCustomFont', serif;
}
第 4 步:HTML 页面
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>字体分块示例</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>你好,世界!(Hello World!)</h1>
<p>
这是一个字体分块的简单示例。
This is a simple example of font chunking.
数字 12345。
常用标点:,。?!
</p>
<p>
这里包含一些汉字,比如"体验" (U+4F53 U+9A8C) 和 "范围" (U+8303 U+56F4)。
</p>
</body>
</html>
浏览器行为解释(结合按需加载机制):
- 浏览器解析 CSS,记录 下
MyCustomFont
的所有@font-face
规则及其unicode-range
。此时不下载任何字体文件。 - 渲染
<h1>
和<p>
中的文本 "你好,世界!(Hello World!) 这是一个..."。 - 遇到 'H' (U+0048),匹配
unicode-range: U+0020-007E;
。检查myfont-latin.woff2
是否已下载?否 -> 发起下载myfont-latin.woff2
。 - 遇到 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', '(', ')' 等,都在
U+0020-U+007E
范围,使用已发起下载或已完成下载的myfont-latin.woff2
,不重复触发下载。 - 遇到 '你' (U+4F60),匹配
unicode-range: U+4E00-62FF;
。检查myfont-hanzi-1.woff2
是否已下载?否 -> 发起下载myfont-hanzi-1.woff2
。 - 遇到 '好' (U+597D), '这' (U+8FD9), '是' (U+662F), '个' (U+4E2A), '字' (U+5B57), '体' (U+4F53), '分' (U+5206), '块' (U+5757), '简' (U+7B80), '单' (U+5355), '示' (U+793A), '例' (U+4F8B), '体' (U+4F53), '验' (U+9A8C), '范' (U+8303), '围' (U+56F4) 等汉字:
- 落在
U+4E00-62FF
的(如 '你', '好', '个', '字', '体', '分', '块', '例'):使用已发起或已完成的myfont-hanzi-1.woff2
。 - 落在
U+6300-7FFF
的(如 '是', '简', '示'):检查myfont-hanzi-2.woff2
是否下载?否 -> 发起下载myfont-hanzi-2.woff2
。 - 落在
U+8000-9FA5
的(如 '这', '验', '范', '围'):检查myfont-hanzi-3.woff2
是否下载?否 -> 发起下载myfont-hanzi-3.woff2
。
- 落在
- 遇到 ',' (U+FF0C), '。' (U+3002), '?' (U+FF1F), '!' (U+FF01) 等标点。假设它们未包含在基础拉丁块中(实际情况可能复杂,取决于字体本身),并且我们定义了
myfont-cjk-punct.woff2
包含U+3000-U+303F
等范围。- 遇到 '。' (U+3002),匹配
unicode-range: U+3000-303F;
。检查myfont-cjk-punct.woff2
是否下载?否 -> 发起下载myfont-cjk-punct.woff2
。 - 其他标点如果也在这个范围,则复用。
- 遇到 '。' (U+3002),匹配
- 结果: 浏览器仅根据页面实际出现的字符 ,触发了对应
unicode-range
的字体块下载请求(在这个例子中是myfont-latin.woff2
,myfont-hanzi-1.woff2
,myfont-hanzi-2.woff2
,myfont-hanzi-3.woff2
,myfont-cjk-punct.woff2
)。
因此,浏览器无需下载 那个庞大的原始 15MB MyCustomFont.ttf
文件(因为它并未在 @font-face
的 src
中直接引用)。更重要的是,只有当页面上实际包含了某个字体块 unicode-range
所覆盖的字符时,该字体块才会被下载 。如果一个页面恰好需要所有分块范围内的字符,那么理论上所有分块都会被下载,但这仍然远优于一次性下载整个未分块的巨大字体。对于那些其 unicode-range
内的字符完全没有在当前页面使用 的字体块,浏览器是不会去下载它们的,从而实现了有效的按需加载和带宽节省。
优点
- 显著减少首屏加载时间: 利用浏览器
unicode-range
的特性实现按需(懒)加载。 - 节省用户带宽: 避免下载未使用的字形数据。
- 改善用户体验: 更快地显示文本,减少 FOUT/FOIT。
- 灵活性: 可以根据需求设计分块策略。
注意事项与缺点
- 分块策略的复杂性: 合理划分
unicode-range
需要权衡,过细可能增加请求数,过粗则优化效果打折。 - 工具依赖: 需要字体工具生成子集。
- 维护成本: 字体更新或策略调整需重新生成。
- 覆盖不全风险:
unicode-range
定义若遗漏字符,会导致回退。 - 动态内容: 对无法预知字符的内容,可能需更保守分块或结合其他技术。
总结来说,字体分块是一种利用 CSS unicode-range
让浏览器智能地按需下载字体片段的高效 Web 字体优化技术,特别适合大型字体库,能大幅提升性能。关键在于理解浏览器并非预先加载所有块,而是根据页面渲染需求实时、精确地加载所需部分。