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

14 KiB
Raw Blame History

方案1-第2步:字体子集化优化 - 实用指南

🎯 优化目标

将自定义中文字体从 ~1.5MB 减少到 ~200KB,提升 80%+


📋 前置准备

检查当前字体文件

# 查看字体文件大小
ls -lh themes/Ying/static/font/zql-v2.woff2
ls -lh themes/Ying/static/font/zql-v2.woff

# 预期大小:800KB - 2MB(包含完整中文字符集)

备份原始字体文件

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(推荐)

# 全局安装
npm install -g glyphhanger

# 验证安装
glyphhanger --version

方法2:使用fonttools(Python)

# 安装Python工具
pip install fonttools brotli

# 验证安装
python3 -c "from fontTools.ttLib import TTFont; print('fonttools installed')"

🚀 实施步骤

步骤1:构建Hugo站点

cd E:\GitHub\blog

# 构建静态文件
hugo --destination=public

# 确认构建成功
ls -la public/

步骤2:运行字体子集化

方法1:使用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/

# 这会生成:
# - zql-v2-subset.woff2(子集字体)
# - zql-v2-subset.woff(子集字体降级格式)

方法2:使用Python脚本(更灵活)

创建文件:scripts/subset-font.py

#!/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()

运行脚本:

# 确保已构建Hugo
hugo --destination=public

# 运行字体子集化脚本
python3 scripts/subset-font.py

步骤3:验证生成的子集字体

# 检查生成的文件
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行的字体声明:

@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;
}

/* 可选:保留原始字体作为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

# 清理缓存
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测试

# 运行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:扩大字符范围

# 修改Python脚本,扫描更多目录
scan_dirs = ["public", "content", "layouts", "static"]

# 或者添加常用字符
common_chars = "的一是不了人我在有他这为之大来以个中上们到说国和地也子时"
chars.update(set(common_chars))

方案B:使用更大的字符集

# 下载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

/* 在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路径不正确或文件未生成

解决方案:

# 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:子集化后文件仍然很大

症状: 生成的子集字体大小与原始差不多

原因: 可能扫描了太多字符

解决方案:

# 查看提取了多少字符
wc -c themes/Ying/static/font/used_chars.txt

# 如果字符数 > 5000,说明范围太广
# 修改脚本,只提取实际使用的字符

问题4:Python脚本运行失败

症状: 报错缺少依赖或路径错误

解决方案:

# 确保安装了所有依赖
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/

🔄 回滚方案

如果字体优化后出现问题,立即回滚:

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. 定期更新子集字体

每当网站内容大幅更新时,重新运行子集化:

# 每月或每季度运行一次
cd E:\GitHub\blog
hugo --destination=public
python3 scripts/subset-font.py

2. 监控字体大小

在部署前检查字体文件大小:

# 如果字体 > 500KB,可能需要优化
ls -lh themes/Ying/static/font/zql-v2-subset.woff2

3. 使用CDN缓存

确保字体文件通过CDN分发:

# Nginx配置
location ~* \.(woff2|woff)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

🎉 优化完成

完成字体子集化后,你的网站将获得:

  • ⚡ 字体大小减少80%+
  • ⚡ 加载速度提升80%+
  • ⚡ 移动端体验显著提升
  • ⚡ 用户流量节省80%+

结合JS优化,总体性能提升约 40-50%!


文档版本: v1.0 创建时间: 2026-06-03 适用方案: 方案1(保守优化)