# 方案1-第2步:字体子集化优化 - 实用指南 ## 🎯 优化目标 将自定义中文字体从 **~1.5MB** 减少到 **~200KB**,提升 80%+ --- ## 📋 前置准备 ### 检查当前字体文件 ```bash # 查看字体文件大小 ls -lh themes/Ying/static/font/zql-v2.woff2 ls -lh themes/Ying/static/font/zql-v2.woff # 预期大小:800KB - 2MB(包含完整中文字符集) ``` ### 备份原始字体文件 ```bash cd E:\GitHub\blog # 创建备份 cp themes/Ying/static/font/zql-v2.woff2 themes/Ying/static/font/zql-v2.woff2.backup cp themes/Ying/static/font/zql-v2.woff themes/Ying/static/font/zql-v2.woff.backup # 确认备份成功 ls -lh themes/Ying/static/font/*.backup ``` --- ## 🔧 安装工具 ### 方法1:使用glyphhanger(推荐) ```bash # 全局安装 npm install -g glyphhanger # 验证安装 glyphhanger --version ``` ### 方法2:使用fonttools(Python) ```bash # 安装Python工具 pip install fonttools brotli # 验证安装 python3 -c "from fontTools.ttLib import TTFont; print('fonttools installed')" ``` --- ## 🚀 实施步骤 ### 步骤1:构建Hugo站点 ```bash cd E:\GitHub\blog # 构建静态文件 hugo --destination=public # 确认构建成功 ls -la public/ ``` ### 步骤2:运行字体子集化 #### 方法1:使用glyphhanger(简单) ```bash # 从本地文件扫描并生成子集 glyphhanger \ --subset=themes/Ying/static/font/zql-v2.woff2 \ --formats=woff2,woff \ --CSS=public/css/*.css \ --HTML=public/**/*.html \ --output=themes/Ying/static/font/ # 这会生成: # - zql-v2-subset.woff2(子集字体) # - zql-v2-subset.woff(子集字体降级格式) ``` #### 方法2:使用Python脚本(更灵活) 创建文件:`scripts/subset-font.py` ```python #!/usr/bin/env python3 """ 字体子集化脚本 从HTML和CSS文件中提取使用的字符,生成子集字体 """ import os import re import sys from fontTools.ttLib import TTFont from fontTools.subset import Subsetter, Options def extract_chars_from_files(directories): """从文件中提取使用的字符""" chars = set() for directory in directories: if not os.path.exists(directory): print(f"⚠️ 目录不存在: {directory}") continue for root, dirs, files in os.walk(directory): for file in files: if file.endswith(('.html', '.md', '.css')): filepath = os.path.join(root, file) try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() # 提取中文字符 chinese_chars = re.findall(r'[一-鿿]', content) chars.update(chinese_chars) # 提取中文标点 cjk_punct = re.findall(r'[ -〿＀-￯]', content) chars.update(cjk_punct) # 提取英文和数字 ascii_chars = re.findall(r'[a-zA-Z0-9]', content) chars.update(ascii_chars) # 提取常用英文标点 en_punct = re.findall(r'[!@#$%^&*()_+\-=\[\]{};:\'",.<>?/\\|`~]', content) chars.update(en_punct) except Exception as e: print(f"⚠️ 读取文件失败 {filepath}: {e}") return chars def subset_font(input_font_path, output_font_path, chars): """生成子集字体""" print(f"📦 加载字体: {input_font_path}") # 加载原始字体 font = TTFont(input_font_path) # 获取原始字符数 cmap = font.getBestCmap() original_count = len(cmap) if cmap else 0 # 配置子集化选项 options = Options() if output_font_path.endswith('.woff2'): options.flavor = 'woff2' elif output_font_path.endswith('.woff'): options.flavor = 'woff' options.desubroutinize = True # 创建子集化器 subsetter = Subsetter(options=options) subsetter.populate(text=''.join(chars)) # 执行子集化 print("✂️ 正在生成子集字体...") subsetter.subset(font) # 保存子集字体 font.save(output_font_path) # 获取子集字符数 subset_font = TTFont(output_font_path) subset_cmap = subset_font.getBestCmap() subset_count = len(subset_cmap) if subset_cmap else 0 # 计算大小 original_size = os.path.getsize(input_font_path) subset_size = os.path.getsize(output_font_path) reduction = original_size - subset_size percentage = (reduction / original_size) * 100 if original_size > 0 else 0 print(f"\n✅ 子集化完成!") print(f"📊 统计信息:") print(f" 原始字符数: {original_count}") print(f" 子集字符数: {subset_count}") print(f" 原始大小: {original_size / 1024:.1f} KB") print(f" 子集大小: {subset_size / 1024:.1f} KB") print(f" 减少: {reduction / 1024:.1f} KB ({percentage:.1f}%)") def main(): # 配置路径 font_dir = "themes/Ying/static/font" input_font = os.path.join(font_dir, "zql-v2.woff2") # 检查输入文件 if not os.path.exists(input_font): print(f"❌ 字体文件不存在: {input_font}") sys.exit(1) # 扫描目录 scan_dirs = ["public", "content", "layouts"] print(f"🔍 扫描目录: {', '.join(scan_dirs)}") # 提取字符 chars = extract_chars_from_files(scan_dirs) print(f"📝 提取了 {len(chars)} 个唯一字符") if len(chars) == 0: print("❌ 未找到任何字符,请检查扫描目录") sys.exit(1) # 保存字符列表(用于调试) chars_file = os.path.join(font_dir, "used_chars.txt") with open(chars_file, 'w', encoding='utf-8') as f: f.write(''.join(sorted(chars))) print(f"💾 字符列表已保存到: {chars_file}") # 生成woff2子集字体 output_woff2 = os.path.join(font_dir, "zql-v2-subset.woff2") subset_font(input_font, output_woff2, chars) # 生成woff子集字体 output_woff = os.path.join(font_dir, "zql-v2-subset.woff") subset_font(input_font, output_woff, chars) print(f"\n🎉 所有子集字体生成完成!") print(f"📂 文件位置: {font_dir}") if __name__ == "__main__": main() ``` 运行脚本: ```bash # 确保已构建Hugo hugo --destination=public # 运行字体子集化脚本 python3 scripts/subset-font.py ``` ### 步骤3:验证生成的子集字体 ```bash # 检查生成的文件 ls -lh themes/Ying/static/font/zql-v2-subset.* # 预期大小: # zql-v2-subset.woff2: 100-300KB(比原始减少80%+) # zql-v2-subset.woff: 150-400KB ``` --- ## 📝 更新CSS字体声明 ### 修改main.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; } /* 可选:保留原始字体作为fallback(如果子集遗漏字符) */ /* @font-face { font-family: 'zql-full'; 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-full', serif; } */ ``` --- ## 🧪 测试验证 ### 步骤1:重新构建Hugo ```bash # 清理缓存 hugo --cleanDestinationDir # 重新构建 hugo --destination=public # 启动开发服务器 hugo server -D ``` ### 步骤2:检查字体加载 #### Network面板验证 1. 打开Chrome DevTools(F12) 2. 切换到 **Network** 面板 3. 刷新页面 4. 查找字体文件(.woff2, .woff) **预期结果:** - ✅ 看到 `zql-v2-subset.woff2` 加载 - ✅ 文件大小显著减少(<300KB) - ✅ 无404错误 #### 视觉检查 **中文字符测试:** - [ ] 常用汉字正常(你好世界、欢迎访问) - [ ] 文章内容中的中文正常 - [ ] 导航菜单中文正常 - [ ] 深色模式下中文正常 **英文字符测试:** - [ ] 英文字母正常(A-Z, a-z) - [ ] 数字正常(0-9) - [ ] 常用符号正常(@#$%) **特殊字符测试:** - [ ] 中文标点正常(,。!?、;:) - [ ] 英文标点正常(,.!?;:) - [ ] 其他符号正常(如果文章中有) ### 步骤3:不同页面测试 - [ ] 首页字体正常 - [ ] 文章详情页字体正常 - [ ] 友链页面字体正常 - [ ] 归档页面字体正常 - [ ] 移动端字体正常 ### 步骤4:Lighthouse测试 ```bash # 运行Lighthouse审计 # 预期:Performance得分提升,无字体相关警告 ``` --- ## 📊 预期优化效果 ### 文件大小对比 | 文件 | 优化前 | 优化后 | 减少 | |------|--------|--------|------| | **zql-v2.woff2** | 1.2MB | 150KB | ⚡ -87% | | **zql-v2.woff** | 1.5MB | 200KB | ⚡ -87% | ### 加载时间提升 | 指标 | 优化前 | 优化后 | 提升 | |------|--------|--------|------| | **字体下载(3G)** | ~3s | ~0.5s | ⚡ -83% | | **FCP** | +0.5s | +0.1s | ⚡ -80% | | **字体加载总时间** | ~4s | ~1s | ⚡ -75% | --- ## 🐛 故障排除 ### 问题1:字符显示为方块(□) **症状:** 部分中文字符显示为方块 **原因:** 子集字体未包含该字符 **解决方案:** #### 方案A:扩大字符范围 ```bash # 修改Python脚本,扫描更多目录 scan_dirs = ["public", "content", "layouts", "static"] # 或者添加常用字符 common_chars = "的一是不了人我在有他这为之大来以个中上们到说国和地也子时" chars.update(set(common_chars)) ``` #### 方案B:使用更大的字符集 ```bash # 下载GB2312常用字符列表(6,763个字符) # 手动添加到字符集 # 或者使用CJK字符范围 import unicodedata cjk_chars = [chr(i) for i in range(0x4E00, 0x9FFF+1) if unicodedata.category(chr(i)) != 'Cn'] chars.update(cjk_chars) ``` #### 方案C:保留原始字体作为fallback ```css /* 在main.css中使用font-family fallback */ @font-face { font-family: 'zql-subset'; src: url('../font/zql-v2-subset.woff2') format('woff2'); font-display: swap; } @font-face { font-family: 'zql-full'; src: url('../font/zql-v2.woff2') format('woff2'); font-display: swap; unicode-range: U+4E00-9FFF; /* 仅CJK字符 */ } body { font-family: 'zql-subset', 'zql-full', serif; } ``` ### 问题2:字体文件404错误 **症状:** 浏览器Console显示字体文件404 **原因:** CSS路径不正确或文件未生成 **解决方案:** ```bash # 1. 检查文件是否存在 ls -lh themes/Ying/static/font/zql-v2-subset.* # 2. 确认CSS路径正确 grep -n "zql-v2-subset" themes/Ying/assets/css/main.css # 3. 重新构建Hugo hugo --cleanDestinationDir ``` ### 问题3:子集化后文件仍然很大 **症状:** 生成的子集字体大小与原始差不多 **原因:** 可能扫描了太多字符 **解决方案:** ```bash # 查看提取了多少字符 wc -c themes/Ying/static/font/used_chars.txt # 如果字符数 > 5000,说明范围太广 # 修改脚本,只提取实际使用的字符 ``` ### 问题4:Python脚本运行失败 **症状:** 报错缺少依赖或路径错误 **解决方案:** ```bash # 确保安装了所有依赖 pip install fonttools brotli # 确保在正确的目录运行 cd E:\GitHub\blog python3 scripts/subset-font.py # 如果仍然失败,使用glyphhanger替代 npm install -g glyphhanger glyphhanger --subset=themes/Ying/static/font/zql-v2.woff2 --formats=woff2,woff --CSS=public/css/*.css --HTML=public/**/*.html --output=themes/Ying/static/font/ ``` --- ## 🔄 回滚方案 如果字体优化后出现问题,立即回滚: ```bash cd E:\GitHub\blog # 恢复原始字体文件 cp themes/Ying/static/font/zql-v2.woff2.backup themes/Ying/static/font/zql-v2.woff2 cp themes/Ying/static/font/zql-v2.woff.backup themes/Ying/static/font/zql-v2.woff # 删除子集字体(可选) rm themes/Ying/static/font/zql-v2-subset.* # 恢复CSS字体声明 # 将 main.css 中的字体路径改回原始文件名: # zql-v2-subset.woff2 → zql-v2.woff2 # zql-v2-subset.woff → zql-v2.woff ``` --- ## ✅ 完成确认清单 ### 文件生成 - [ ] `zql-v2-subset.woff2` 已生成(<300KB) - [ ] `zql-v2-subset.woff` 已生成(<400KB) - [ ] `used_chars.txt` 已生成(用于调试) ### CSS更新 - [ ] `main.css` 字体声明已更新 - [ ] 指向新的子集字体文件 ### 测试验证 - [ ] 中文字符显示正常 - [ ] 英文字符显示正常 - [ ] 特殊符号显示正常 - [ ] 所有页面字体正常 - [ ] 深色模式字体正常 - [ ] 移动端字体正常 - [ ] 无Console错误 ### 性能验证 - [ ] Network面板显示字体大小减少 - [ ] Lighthouse无字体相关警告 - [ ] FCP指标改善 --- ## 📈 最佳实践 ### 1. 定期更新子集字体 每当网站内容大幅更新时,重新运行子集化: ```bash # 每月或每季度运行一次 cd E:\GitHub\blog hugo --destination=public python3 scripts/subset-font.py ``` ### 2. 监控字体大小 在部署前检查字体文件大小: ```bash # 如果字体 > 500KB,可能需要优化 ls -lh themes/Ying/static/font/zql-v2-subset.woff2 ``` ### 3. 使用CDN缓存 确保字体文件通过CDN分发: ```nginx # Nginx配置 location ~* \.(woff2|woff)$ { expires 1y; add_header Cache-Control "public, immutable"; } ``` --- ## 🎉 优化完成 完成字体子集化后,你的网站将获得: - ⚡ **字体大小减少80%+** - ⚡ **加载速度提升80%+** - ⚡ **移动端体验显著提升** - ⚡ **用户流量节省80%+** 结合JS优化,总体性能提升约 **40-50%**! --- **文档版本:** v1.0 **创建时间:** 2026-06-03 **适用方案:** 方案1(保守优化)