# 第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文件,提取实际使用的字符,生成子集字体。 **优点:** - 全自动化,无需手动维护 - 准确提取所有使用的字符 - 可集成到构建流程 **工具选项:** 1. **glyphhanger** - Google推荐 2. **fonttools** - Python工具 3. **subfont** - 基于PostCSS ### 策略2:手动字符列表 手动列出常用的中文字符(GB2312常用字约6,763个) **优点:** - 字符集稳定,不会遗漏 - 适合内容变化不大的网站 **缺点:** - 可能包含未使用的字符 - 需要手动维护 --- ## 📝 实施方案:策略1(自动字符提取) ### 步骤3.1:安装glyphhanger ```bash # 使用npm全局安装 npm install -g glyphhanger # 或者使用pip安装fonttools pip install fonttools brotli # 或者使用subfont npm install -g subfont ``` ### 步骤3.2:扫描网站提取字符 #### 方法1:从本地构建文件扫描 ```bash # 先构建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:从在线网站扫描(推荐) ```bash # 扫描在线网站(需要先部署或本地预览) glyphhanger https://your-blog-url.com \ --subset=themes/Ying/static/font/zql-v2.woff2 \ --formats=woff2 \ --output=themes/Ying/static/font/ ``` #### 方法3:使用fonttools手动提取 ```bash # 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行):** ```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; /* 中文标点符号 */ } ``` **修改后代码:** ```css @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行):** ```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; } ``` **修改后代码:** ```css /* 字体声明 - 使用子集字体 */ @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` ```bash #!/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 ``` ### 设置执行权限 ```bash chmod +x themes/Ying/scripts/subset-fonts.sh ``` ### 使用方法 ```bash # 方法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:字符显示为方块(□) **症状:** 某些中文字符显示为方块 **原因:** 子集字体未包含该字符 **解决方案:** 1. 检查生成的字符列表(used_chars.txt) 2. 确认该字符在HTML中存在 3. 重新运行子集化脚本 4. 或者手动添加该字符到字符列表 ### 问题2:字体文件404错误 **症状:** 浏览器Console显示字体文件404 **原因:** CSS中的字体路径不正确 **解决方案:** 1. 检查字体文件是否在正确的目录 2. 确认CSS中的路径正确 3. 检查Hugo的静态文件配置 ### 问题3:字体加载缓慢 **症状:** 字体加载时间没有明显改善 **原因:** 可能是网络问题或CDN配置 **解决方案:** 1. 检查字体文件是否真的减小了 2. 确认CDN缓存已更新 3. 使用浏览器禁用缓存重新测试 ### 问题4:子集化后字符缺失 **症状:** 部分文章的特殊字符显示异常 **解决方案:** ```bash # 方法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 # 重新生成子集字体 ``` --- ## 🔄 回滚方案 如果优化后字体显示异常,快速回滚: ```bash # 恢复原始字体文件 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中的字体声明: ```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. 定期更新子集字体 每当网站内容大幅更新时,重新运行子集化脚本: ```bash # 每月或每季度运行一次 ./themes/Ying/scripts/subset-fonts.sh online https://your-blog.com ``` ### 2. 监控字体大小 在CI/CD流程中添加字体大小检查: ```yaml # .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 # Nginx配置 location ~* \.(woff2|woff)$ { expires 1y; add_header Cache-Control "public, immutable"; } ``` --- ## ✅ 完成确认清单 - [ ] 安装glyphhanger或fonttools - [ ] 运行子集化脚本 - [ ] 生成子集字体文件 - [ ] 更新CSS字体声明 - [ ] 同步更新critical.css - [ ] 测试字体显示 - [ ] 测试不同页面 - [ ] 测试深色模式 - [ ] 使用Lighthouse验证性能提升 - [ ] 提交代码 --- ## 🎉 三步优化完成总结 ### 第1步:CSS加载优化 ✅ - 关键CSS内联到 - 非关键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