Files
blog/themes/Ying/GUIDE_FONT_SUBSETTING.md
T
Vaica cbf2034f78 perf: JS按需加载优化 + PJAX适配
- 首页JS减少56%(800KB → 350KB)
- 文章页JS减少44%(800KB → 450KB)
- 完美适配PJAX导航
- 保持所有功能正常
2026-06-03 12:29:56 +08:00

611 lines
14 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 方案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(保守优化)