15 KiB
第3步:字体子集化优化 - 实施方案
📋 当前字体分析
字体文件清单
1. zql-v2.woff2(自定义中文字体)
位置: themes/Ying/static/font/zql-v2.woff2
当前大小: 约 800KB - 2MB(包含完整中文字符集)
用途: 正文显示(body默认字体)
问题:
- 包含全部中文字符(20,000+)
- 实际使用的字符可能只有500-2000个
- 大量未使用的字符浪费带宽
2. zql-v2.woff(降级格式)
位置: themes/Ying/static/font/zql-v2.woff
当前大小: 与woff2类似
用途: 不支持woff2的浏览器降级
3. remixicon.woff2/woff(图标字体)
位置: themes/Ying/static/font/remixicon.*
大小: 约 100-200KB
用途: UI图标(搜索、主题切换等)
注意: 图标字体通常不建议子集化,因为图标字符集是固定的
🎯 优化策略
策略1:自动字符提取(推荐)
使用工具自动扫描HTML文件,提取实际使用的字符,生成子集字体。
优点:
- 全自动化,无需手动维护
- 准确提取所有使用的字符
- 可集成到构建流程
工具选项:
- glyphhanger - Google推荐
- fonttools - Python工具
- subfont - 基于PostCSS
策略2:手动字符列表
手动列出常用的中文字符(GB2312常用字约6,763个)
优点:
- 字符集稳定,不会遗漏
- 适合内容变化不大的网站
缺点:
- 可能包含未使用的字符
- 需要手动维护
📝 实施方案:策略1(自动字符提取)
步骤3.1:安装glyphhanger
# 使用npm全局安装
npm install -g glyphhanger
# 或者使用pip安装fonttools
pip install fonttools brotli
# 或者使用subfont
npm install -g subfont
步骤3.2:扫描网站提取字符
方法1:从本地构建文件扫描
# 先构建Hugo站点
cd E:\GitHub\blog
hugo
# 使用glyphhanger扫描public目录
glyphhanger --subset=themes/Ying/static/font/zql-v2.woff2 \
--formats=woff2 \
--CSS=public/css/*.css \
--HTML=public/**/*.html \
--output=themes/Ying/static/font/
方法2:从在线网站扫描(推荐)
# 扫描在线网站(需要先部署或本地预览)
glyphhanger https://your-blog-url.com \
--subset=themes/Ying/static/font/zql-v2.woff2 \
--formats=woff2 \
--output=themes/Ying/static/font/
方法3:使用fonttools手动提取
# 1. 提取所有使用的字符到文本文件
python3 << 'EOF'
import os
import re
def extract_chars_from_html(directory):
chars = set()
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith('.html') or file.endswith('.md'):
filepath = os.path.join(root, file)
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 提取中文字符
chinese_chars = re.findall(r'[一-鿿]', content)
chars.update(chinese_chars)
# 提取英文和数字
ascii_chars = re.findall(r'[a-zA-Z0-9]', content)
chars.update(ascii_chars)
# 提取常用标点
punctuation = re.findall(r'[,。!?、;:""''()【】《》]', content)
chars.update(punctuation)
return chars
# 扫描content和layouts目录
content_chars = extract_chars_from_html('content')
layout_chars = extract_chars_from_html('layouts')
all_chars = content_chars | layout_chars
# 保存到文件
with open('used_chars.txt', 'w', encoding='utf-8') as f:
f.write(''.join(sorted(all_chars)))
print(f"提取了 {len(all_chars)} 个字符")
EOF
# 2. 使用fonttools生成子集字体
pip install fonttools brotli
python3 << 'EOF'
from fontTools.ttLib import TTFont
from fontTools.subset import Subsetter, Options
# 读取使用的字符
with open('used_chars.txt', 'r', encoding='utf-8') as f:
used_chars = set(f.read())
# 加载原始字体
font = TTFont('themes/Ying/static/font/zql-v2.woff2')
# 配置子集化选项
options = Options()
options.flavor = 'woff2'
options.desubroutinize = True
# 创建子集化器
subsetter = Subsetter(options=options)
subsetter.populate(text=''.join(used_chars))
# 执行子集化
subsetter.subset(font)
# 保存子集字体
font.save('themes/Ying/static/font/zql-v2-subset.woff2')
print("子集字体生成完成!")
EOF
步骤3.3:更新CSS字体声明
修改文件: themes/Ying/assets/css/main.css
当前代码(第1-14行):
@font-face {
font-family: 'zql';
src: url('../font/zql-v2.woff2') format('woff2'),
url('../font/zql-v2.woff') format('woff');
font-display: swap;
unicode-range: U+0000-007F,
/* 基本拉丁字母 */
U+4E00-9FFF,
/* 基本汉字 */
U+2000-206F,
/* 通用标点符号 */
U+3000-303F;
/* 中文标点符号 */
}
修改后代码:
@font-face {
font-family: 'zql';
src: url('../font/zql-v2-subset.woff2') format('woff2'),
url('../font/zql-v2-subset.woff') format('woff');
font-display: swap;
/* 不再需要unicode-range,因为已经是子集 */
}
/* 保留原始字体作为fallback(可选) */
@font-face {
font-family: 'zql-fallback';
src: url('../font/zql-v2.woff2') format('woff2'),
url('../font/zql-v2.woff') format('woff');
font-display: swap;
unicode-range: U+0000-007F, U+4E00-9FFF, U+2000-206F, U+3000-303F;
}
body {
font-family: 'zql', 'zql-fallback', serif;
}
步骤3.4:同步更新critical.css
修改文件: themes/Ying/assets/css/critical.css
当前代码(第16-23行):
/* 字体声明 */
@font-face {
font-family: 'zql';
src: url('../font/zql-v2.woff2') format('woff2'),
url('../font/zql-v2.woff') format('woff');
font-display: swap;
unicode-range: U+0000-007F, U+4E00-9FFF, U+2000-206F, U+3000-303F;
}
修改后代码:
/* 字体声明 - 使用子集字体 */
@font-face {
font-family: 'zql';
src: url('../font/zql-v2-subset.woff2') format('woff2'),
url('../font/zql-v2-subset.woff') format('woff');
font-display: swap;
}
🔧 自动化脚本
创建构建脚本
文件路径: themes/Ying/scripts/subset-fonts.sh
#!/bin/bash
# 字体子集化自动化脚本
# 用法:./scripts/subset-fonts.sh [online|local]
set -e
MODE=${1:-local}
BLOG_URL=${2:-http://localhost:1313}
FONT_DIR="themes/Ying/static/font"
ORIGINAL_FONT="$FONT_DIR/zql-v2.woff2"
SUBSET_FONT="$FONT_DIR/zql-v2-subset.woff2"
echo "🔤 开始字体子集化..."
echo "模式: $MODE"
# 检查依赖
if ! command -v glyphhanger &> /dev/null; then
echo "❌ glyphhanger未安装,正在安装..."
npm install -g glyphhanger
fi
# 备份原始字体
if [ ! -f "$ORIGINAL_FONT.bak" ]; then
cp "$ORIGINAL_FONT" "$ORIGINAL_FONT.bak"
echo "✅ 原始字体已备份到 $ORIGINAL_FONT.bak"
fi
if [ "$MODE" = "online" ]; then
echo "📡 从在线网站扫描字符..."
glyphhanger "$BLOG_URL" \
--subset="$ORIGINAL_FONT" \
--formats=woff2,woff \
--output="$FONT_DIR/"
else
echo "📁 从本地文件扫描字符..."
# 先构建Hugo
echo "🔨 构建Hugo站点..."
hugo --destination=public_temp
# 扫描HTML和CSS
glyphhanger \
--subset="$ORIGINAL_FONT" \
--formats=woff2,woff \
--CSS=public_temp/css/*.css \
--HTML=public_temp/**/*.html \
--output="$FONT_DIR/"
# 清理临时目录
rm -rf public_temp
fi
# 检查生成的文件
if [ -f "$SUBSET_FONT" ]; then
ORIGINAL_SIZE=$(stat -f%z "$ORIGINAL_FONT" 2>/dev/null || stat -c%s "$ORIGINAL_FONT")
SUBSET_SIZE=$(stat -f%z "$SUBSET_FONT" 2>/dev/null || stat -c%s "$SUBSET_FONT")
REDUCTION=$((ORIGINAL_SIZE - SUBSET_SIZE))
PERCENTAGE=$((REDUCTION * 100 / ORIGINAL_SIZE))
echo ""
echo "✅ 字体子集化完成!"
echo "📊 大小对比:"
echo " 原始字体: $(numfmt --to=iec $ORIGINAL_SIZE)"
echo " 子集字体: $(numfmt --to=iec $SUBSET_SIZE)"
echo " 减少: $(numfmt --to=iec $REDUCTION) ($PERCENTAGE%)"
echo ""
echo "📝 下一步:"
echo " 1. 更新CSS中的字体路径为 zql-v2-subset.woff2"
echo " 2. 测试网站显示是否正常"
echo " 3. 提交代码"
else
echo "❌ 子集字体生成失败"
exit 1
fi
设置执行权限
chmod +x themes/Ying/scripts/subset-fonts.sh
使用方法
# 方法1:从本地文件扫描(推荐开发时使用)
./themes/Ying/scripts/subset-fonts.sh local
# 方法2:从在线网站扫描(推荐部署前使用)
./themes/Ying/scripts/subset-fonts.sh online https://your-blog.com
📊 预期收益
文件大小对比(预估)
| 字体文件 | 优化前 | 优化后 | 减少 |
|---|---|---|---|
| zql-v2.woff2 | 1.2MB | 150KB | ⚡ -87% |
| zql-v2.woff | 1.5MB | 200KB | ⚡ -87% |
加载时间提升
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 字体下载时间 | ~3s (3G) | ~0.5s (3G) | ⚡ -83% |
| FCP | +0.5s | +0.1s | ⚡ -80% |
| FOIT时间 | 长 | 短 | ⚡ 显著改善 |
用户体验提升
- ✅ 字体加载更快
- ✅ 文字闪烁减少(FOIT/FOUT)
- ✅ 移动端体验更好(节省流量)
- ✅ 首屏渲染更快
🧪 测试验证清单
功能测试
1. 字体显示测试
- 中文字符正确显示
- 常用汉字(你好世界)
- 生僻字(如果文章中有)
- 中文标点符号(,。!?)
- 英文字符正确显示
- 大写字母(A-Z)
- 小写字母(a-z)
- 数字(0-9)
- 特殊符号正确显示
- 常用符号(@#$%)
- 数学符号(如果文章中有)
2. 不同页面测试
- 首页文章列表字体正常
- 文章详情页字体正常
- 友链页面字体正常
- 归档页面字体正常
- 移动端字体正常
3. 深色模式测试
- 深色模式下字体正常
- 切换主题时字体无闪烁
性能测试
1. Network面板检查
- 字体文件大小显著减小
- 字体加载时间缩短
- 无404错误(字体文件路径正确)
2. Lighthouse检查
- Performance得分提升
- FCP指标改善
- 无字体相关警告
兼容性测试
浏览器测试
- Chrome(最新版)
- Firefox(最新版)
- Safari(最新版)
- Edge(最新版)
- 移动端Chrome
- 移动端Safari
🐛 故障排除
问题1:字符显示为方块(□)
症状: 某些中文字符显示为方块
原因: 子集字体未包含该字符
解决方案:
- 检查生成的字符列表(used_chars.txt)
- 确认该字符在HTML中存在
- 重新运行子集化脚本
- 或者手动添加该字符到字符列表
问题2:字体文件404错误
症状: 浏览器Console显示字体文件404
原因: CSS中的字体路径不正确
解决方案:
- 检查字体文件是否在正确的目录
- 确认CSS中的路径正确
- 检查Hugo的静态文件配置
问题3:字体加载缓慢
症状: 字体加载时间没有明显改善
原因: 可能是网络问题或CDN配置
解决方案:
- 检查字体文件是否真的减小了
- 确认CDN缓存已更新
- 使用浏览器禁用缓存重新测试
问题4:子集化后字符缺失
症状: 部分文章的特殊字符显示异常
解决方案:
# 方法1:扩大字符范围
# 修改glyphhanger命令,增加更多页面
glyphhanger https://your-blog.com/sitemap.xml \
--subset=themes/Ying/static/font/zql-v2.woff2 \
--formats=woff2,woff \
--output=themes/Ying/static/font/
# 方法2:使用更大的字符集(GB2312)
# 下载GB2312字符列表
curl -o gb2312_chars.txt https://example.com/gb2312_chars.txt
# 添加到字符集
cat gb2312_chars.txt >> used_chars.txt
sort -u used_chars.txt -o used_chars.txt
# 重新生成子集字体
🔄 回滚方案
如果优化后字体显示异常,快速回滚:
# 恢复原始字体文件
cp themes/Ying/static/font/zql-v2.woff2.bak themes/Ying/static/font/zql-v2.woff2
cp themes/Ying/static/font/zql-v2.woff.bak themes/Ying/static/font/zql-v2.woff
# 恢复CSS字体声明
# 将 zql-v2-subset.woff2 改回 zql-v2.woff2
或者恢复CSS中的字体声明:
/* 回滚到原始字体 */
@font-face {
font-family: 'zql';
src: url('../font/zql-v2.woff2') format('woff2'),
url('../font/zql-v2.woff') format('woff');
font-display: swap;
unicode-range: U+0000-007F, U+4E00-9FFF, U+2000-206F, U+3000-303F;
}
📈 最佳实践
1. 定期更新子集字体
每当网站内容大幅更新时,重新运行子集化脚本:
# 每月或每季度运行一次
./themes/Ying/scripts/subset-fonts.sh online https://your-blog.com
2. 监控字体大小
在CI/CD流程中添加字体大小检查:
# .github/workflows/build.yml
- name: Check font size
run: |
FONT_SIZE=$(stat -c%s themes/Ying/static/font/zql-v2-subset.woff2)
if [ $FONT_SIZE -gt 500000 ]; then
echo "⚠️ 字体文件过大:$(numfmt --to=iec $FONT_SIZE)"
exit 1
fi
3. 使用CDN缓存
确保字体文件通过CDN分发,并设置长期缓存:
# Nginx配置
location ~* \.(woff2|woff)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
✅ 完成确认清单
- 安装glyphhanger或fonttools
- 运行子集化脚本
- 生成子集字体文件
- 更新CSS字体声明
- 同步更新critical.css
- 测试字体显示
- 测试不同页面
- 测试深色模式
- 使用Lighthouse验证性能提升
- 提交代码
🎉 三步优化完成总结
第1步:CSS加载优化 ✅
- 关键CSS内联到<head>
- 非关键CSS异步加载
- 预期FCP提升20-30%
第2步:JS按需加载优化 ✅
- 核心JS始终加载
- 页面特定JS按需加载
- 非关键JS延迟加载
- 预期TTI提升25-35%
第3步:字体子集化优化 ⏳
- 自动提取实际使用的字符
- 生成子集字体
- 预期字体大小减少80-90%
📊 总体性能提升预估
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| CSS大小 | ~500KB | ~20KB (首屏) | ⚡ -96% |
| JS大小 | ~800KB | ~350KB (首页) | ⚡ -56% |
| 字体大小 | ~1.5MB | ~200KB | ⚡ -87% |
| FCP | ~2.5s | ~1.2s | ⚡ -52% |
| TTI | ~4s | ~2s | ⚡ -50% |
| Lighthouse得分 | ~60 | ~90+ | ⚡ +50% |
文档版本: v1.0 创建日期: 2026-06-03 优化工具: Claude AI