前端资源优化之【字体子集化】

342 阅读19分钟

字体子集化

字体子集化是现代Web性能优化的重要技术之一。通过提取网页实际使用的字符,生成包含最小字符集的字体文件,可以显著减少字体文件大小,提升页面加载速度和用户体验。

适用于服务端渲染的场景

字体子集化的概念

字体子集化(Font Subsetting)是从完整字体文件中提取特定字符集的过程,生成只包含页面实际需要字符的精简字体文件。

字体优化原理

graph TD
    A["完整字体文件<br/>包含所有字符"] --> B["分析页面内容"]
    B --> C["提取使用的字符"]
    C --> D["生成字体子集"]
    D --> E["压缩和格式转换"]
    E --> F["优化后的字体文件<br/>体积减少60-90%"]
    
    B --> B1["扫描HTML内容"]
    B --> B2["分析CSS样式"]
    B --> B3["检查JavaScript动态内容"]
    
    D --> D1["包含必需字符"]
    D --> D2["保留字体元信息"]
    D --> D3["维护字符映射关系"]
    
    E --> E1["WOFF2压缩"]
    E --> E2["WOFF格式"]
    E --> E3["TTF格式"]
    E --> E4["移除未使用的表"]
    
    style A fill:#ffebee
    style F fill:#e8f5e8
    style B fill:#e3f2fd
    style D fill:#fff3e0
    style E fill:#f3e5f5

字体文件结构分析

现代字体文件包含大量信息,理解其结构有助于优化策略的制定:

// 字体文件结构分析器
class FontAnalyzer {
  constructor() {
    this.supportedFormats = ['ttf', 'otf', 'woff', 'woff2'];
    this.fontMetrics = {
      glyphCount: 0,
      fileSize: 0,
      compressionRatio: 0,
      unicodeRanges: [],
      features: []
    };
  }

  // 分析字体文件基本信息
  async analyzeFontFile(fontBuffer) {
    try {
      const fontInfo = {
        originalSize: fontBuffer.byteLength,
        format: this.detectFontFormat(fontBuffer),
        glyphs: await this.extractGlyphInfo(fontBuffer),
        metadata: await this.extractMetadata(fontBuffer)
      };

      return this.generateAnalysisReport(fontInfo);
    } catch (error) {
      console.error('字体分析失败:', error);
      throw error;
    }
  }

  // 检测字体格式
  detectFontFormat(buffer) {
    const view = new DataView(buffer);
    const signature = view.getUint32(0);
    
    // WOFF2 signature
    if (signature === 0x774F4632) return 'woff2';
    
    // WOFF signature  
    if (signature === 0x774F4646) return 'woff';
    
    // TTF/OTF signatures
    if (signature === 0x00010000 || signature === 0x74727565) return 'ttf';
    if (signature === 0x4F54544F) return 'otf';
    
    return 'unknown';
  }

  // 提取字形信息
  async extractGlyphInfo(fontBuffer) {
    // 这里使用 opentype.js 或类似库解析字体
    const font = await this.parseFont(fontBuffer);
    
    return {
      totalGlyphs: font.numGlyphs,
      unicodeRanges: this.getUnicodeRanges(font),
      characterSet: this.extractCharacterSet(font),
      languageSupport: this.detectLanguageSupport(font)
    };
  }

  // 提取字符集
  extractCharacterSet(font) {
    const chars = new Set();
    
    if (font.tables.cmap) {
      for (const [unicode, glyphIndex] of font.tables.cmap.glyphIndexMap) {
        if (glyphIndex > 0) {
          chars.add(String.fromCharCode(unicode));
        }
      }
    }
    
    return Array.from(chars).sort();
  }

  // 检测语言支持
  detectLanguageSupport(font) {
    const support = {
      latin: false,
      chinese: false,
      japanese: false,
      korean: false,
      arabic: false,
      cyrillic: false
    };

    const chars = this.extractCharacterSet(font);
    
    chars.forEach(char => {
      const code = char.charCodeAt(0);
      
      // 基本拉丁字母
      if (code >= 0x0020 && code <= 0x007F) support.latin = true;
      
      // 中文字符
      if ((code >= 0x4E00 && code <= 0x9FFF) || 
          (code >= 0x3400 && code <= 0x4DBF)) support.chinese = true;
      
      // 日文字符
      if ((code >= 0x3040 && code <= 0x309F) || 
          (code >= 0x30A0 && code <= 0x30FF)) support.japanese = true;
      
      // 韩文字符
      if (code >= 0xAC00 && code <= 0xD7AF) support.korean = true;
      
      // 阿拉伯文字符
      if (code >= 0x0600 && code <= 0x06FF) support.arabic = true;
      
      // 西里尔字符
      if (code >= 0x0400 && code <= 0x04FF) support.cyrillic = true;
    });

    return support;
  }

  // 生成分析报告
  generateAnalysisReport(fontInfo) {
    const report = {
      基本信息: {
        文件大小: `${(fontInfo.originalSize / 1024).toFixed(2)} KB`,
        字体格式: fontInfo.format,
        字形数量: fontInfo.glyphs.totalGlyphs,
        字符数量: fontInfo.glyphs.characterSet.length
      },
      字符支持: fontInfo.glyphs.languageSupport,
      Unicode范围: fontInfo.glyphs.unicodeRanges,
      优化建议: this.generateOptimizationSuggestions(fontInfo)
    };

    return report;
  }

  // 生成优化建议
  generateOptimizationSuggestions(fontInfo) {
    const suggestions = [];
    
    if (fontInfo.originalSize > 100 * 1024) {
      suggestions.push('文件较大,建议进行子集化处理');
    }
    
    if (fontInfo.glyphs.totalGlyphs > 1000) {
      suggestions.push('字形数量较多,可以移除未使用的字符');
    }
    
    if (fontInfo.format !== 'woff2') {
      suggestions.push('建议转换为WOFF2格式以获得更好的压缩率');
    }
    
    const unusedLanguages = Object.entries(fontInfo.glyphs.languageSupport)
      .filter(([lang, supported]) => supported && lang !== 'latin')
      .map(([lang]) => lang);
    
    if (unusedLanguages.length > 0) {
      suggestions.push(`检查是否需要${unusedLanguages.join('、')}语言支持`);
    }

    return suggestions;
  }
}

// 使用示例
const fontAnalyzer = new FontAnalyzer();

// 分析字体文件
async function analyzeFont(fontPath) {
  try {
    const fontBuffer = await fetch(fontPath).then(res => res.arrayBuffer());
    const analysis = await fontAnalyzer.analyzeFontFile(fontBuffer);
    
    console.log('字体分析结果:', analysis);
    return analysis;
  } catch (error) {
    console.error('字体分析失败:', error);
  }
}

字体优化策略

制定合适的字体优化策略需要考虑多个维度:

优化策略选择流程

graph TD
    A["开始优化"] --> B{分析网站类型}
    
    B -->|静态内容网站| C["静态字符集提取"]
    B -->|动态内容网站| D["动态字符集分析"]
    B -->|多语言网站| E["语言分离策略"]
    
    C --> C1["扫描所有HTML/CSS"]
    C --> C2["提取使用的字符"]
    C --> C3["生成静态子集"]
    
    D --> D1["监控用户输入"]
    D --> D2["分析API返回内容"]
    D --> D3["动态加载字符"]
    
    E --> E1["按语言分割字体"]
    E --> E2["按需加载语言包"]
    E --> E3["智能语言检测"]
    
    C3 --> F["格式转换"]
    D3 --> F
    E3 --> F
    
    F --> F1["生成WOFF2"]
    F --> F2["生成WOFF降级"]
    F --> F3["压缩优化"]
    
    F1 --> G["部署策略"]
    F2 --> G
    F3 --> G
    
    G --> G1["CDN分发"]
    G --> G2["预加载关键字体"]
    G --> G3["字体显示策略"]
    
    style A fill:#e3f2fd
    style F fill:#c8e6c9
    style G fill:#fff3e0

综合优化策略实现

// 字体优化策略管理器
class FontOptimizationStrategy {
  constructor(options = {}) {
    this.options = {
      targetLanguages: options.targetLanguages || ['zh-CN', 'en'],
      compressionLevel: options.compressionLevel || 'high',
      fallbackStrategy: options.fallbackStrategy || 'progressive',
      cacheStrategy: options.cacheStrategy || 'aggressive',
      ...options
    };
    
    this.optimizationPipeline = [];
    this.setupOptimizationPipeline();
  }

  // 设置优化流水线
  setupOptimizationPipeline() {
    this.optimizationPipeline = [
      this.analyzeContent.bind(this),
      this.extractCharacterSet.bind(this),
      this.generateSubsets.bind(this),
      this.compressAndConvert.bind(this),
      this.generateFallbacks.bind(this),
      this.optimizeLoading.bind(this)
    ];
  }

  // 内容分析
  async analyzeContent(input) {
    const { htmlContent, cssContent, jsContent } = input;
    
    const analysis = {
      staticChars: new Set(),
      dynamicPatterns: [],
      languageDistribution: {},
      estimatedUsage: {}
    };

    // 分析HTML内容
    const htmlChars = this.extractHTMLCharacters(htmlContent);
    htmlChars.forEach(char => analysis.staticChars.add(char));

    // 分析CSS内容
    const cssChars = this.extractCSSCharacters(cssContent);
    cssChars.forEach(char => analysis.staticChars.add(char));

    // 分析JavaScript动态内容
    const dynamicPatterns = this.analyzeDynamicContent(jsContent);
    analysis.dynamicPatterns = dynamicPatterns;

    // 语言分布分析
    analysis.languageDistribution = this.analyzeLanguageDistribution(
      Array.from(analysis.staticChars)
    );

    return { ...input, analysis };
  }

  // 提取HTML字符
  extractHTMLCharacters(htmlContent) {
    const chars = new Set();
    
    // 移除HTML标签,只保留文本内容
    const textContent = htmlContent.replace(/<[^>]*>/g, '');
    
    // 解码HTML实体
    const decodedContent = this.decodeHTMLEntities(textContent);
    
    for (const char of decodedContent) {
      if (char.trim()) {
        chars.add(char);
      }
    }
    
    return Array.from(chars);
  }

  // 提取CSS字符
  extractCSSCharacters(cssContent) {
    const chars = new Set();
    
    // 提取CSS中的字符,特别是content属性和字体族名称
    const contentMatches = cssContent.match(/content:\s*["']([^"']+)["']/g) || [];
    const fontFamilyMatches = cssContent.match(/font-family:\s*["']([^"']+)["']/g) || [];
    
    [...contentMatches, ...fontFamilyMatches].forEach(match => {
      const content = match.match(/["']([^"']+)["']/)[1];
      for (const char of content) {
        chars.add(char);
      }
    });
    
    return Array.from(chars);
  }

  // 分析动态内容
  analyzeDynamicContent(jsContent) {
    const patterns = [];
    
    // 分析字符串模板
    const templateMatches = jsContent.match(/`[^`]*`/g) || [];
    templateMatches.forEach(template => {
      patterns.push({
        type: 'template',
        content: template,
        estimatedChars: this.extractTemplateChars(template)
      });
    });

    // 分析API调用
    const apiMatches = jsContent.match(/fetch\([^)]+\)/g) || [];
    patterns.push({
      type: 'api',
      calls: apiMatches,
      recommendation: '建议监控API返回内容的字符使用情况'
    });

    return patterns;
  }

  // 生成优化报告
  generateOptimizationReport(result) {
    const { analysis, subsets, optimizedFonts, loadingOptimization } = result;
    
    return {
      summary: {
        totalCharacters: analysis.staticChars.size,
        subsetsGenerated: Object.keys(subsets).length,
        estimatedSavings: this.calculateSavings(optimizedFonts),
        loadingStrategy: loadingOptimization.fontDisplay
      },
      details: {
        characterDistribution: analysis.languageDistribution,
        subsetBreakdown: Object.entries(subsets).map(([name, chars]) => ({
          name,
          characterCount: chars.length,
          estimatedSize: `${(chars.length * 120 / 1024).toFixed(2)} KB`
        })),
        recommendedImplementation: this.generateImplementationGuide(result)
      }
    };
  }

  // 计算节省的空间
  calculateSavings(optimizedFonts) {
    const originalSize = 2000; // 假设原始字体2MB
    const optimizedSize = Object.values(optimizedFonts)
      .reduce((total, font) => total + font.formats.woff2.size, 0);
    
    const savings = ((originalSize * 1024 - optimizedSize) / (originalSize * 1024) * 100).toFixed(1);
    return `${savings}% (${((originalSize * 1024 - optimizedSize) / 1024).toFixed(2)} KB)`;
  }
}

// 使用示例
const optimizer = new FontOptimizationStrategy({
  targetLanguages: ['zh-CN', 'en'],
  compressionLevel: 'high',
  fallbackStrategy: 'progressive'
});

使用 Fontmin 进行字体子集化

Fontmin 是一个专业的字体子集化工具,能够高效地提取和优化字体文件。

安装 Fontmin

# 安装 Fontmin CLI 工具
npm install -g fontmin

# 或者在项目中安装
npm install fontmin --save-dev

# 安装相关插件
npm install fontmin-webpack --save-dev

使用 Fontmin 进行字体子集化

基础用法示例
// Fontmin 基础使用
const Fontmin = require('fontmin');

class FontminProcessor {
  constructor(options = {}) {
    this.options = {
      srcPath: options.srcPath || './src/fonts',
      destPath: options.destPath || './dist/fonts',
      formats: options.formats || ['ttf', 'woff', 'woff2'],
      ...options
    };
  }

  // 基础字体子集化
  async processBasicSubset(fontPath, characters) {
    const fontmin = new Fontmin()
      .src(fontPath)
      .dest(this.options.destPath)
      .use(Fontmin.glyph({
        text: characters,
        hinting: false // 禁用字体hinting以减小文件大小
      }))
      .use(Fontmin.ttf2woff2())
      .use(Fontmin.ttf2woff())
      .use(Fontmin.css({
        fontFamily: 'OptimizedFont',
        base64: false
      }));

    return new Promise((resolve, reject) => {
      fontmin.run((err, files) => {
        if (err) {
          reject(err);
        } else {
          console.log('字体处理完成:', files.length, '个文件');
          resolve(files);
        }
      });
    });
  }

  // 高级字体处理
  async processAdvancedSubset(fontPath, options = {}) {
    const {
      characters,
      unicodeRange,
      optimizeLevel = 3,
      removeKerning = false,
      removeHinting = true
    } = options;

    const fontmin = new Fontmin()
      .src(fontPath)
      .dest(this.options.destPath);

    // 添加字符提取插件
    if (characters) {
      fontmin.use(Fontmin.glyph({
        text: characters,
        hinting: !removeHinting
      }));
    }

    // Unicode 范围过滤
    if (unicodeRange) {
      fontmin.use(Fontmin.unicode(unicodeRange));
    }

    // 字体优化插件
    fontmin.use(Fontmin.otf2ttf())
      .use(Fontmin.ttf2woff2({
        clone: true
      }))
      .use(Fontmin.ttf2woff({
        clone: true,
        deflate: true
      }))
      .use(Fontmin.css({
        fontFamily: options.fontFamily || 'OptimizedFont',
        base64: false,
        glyph: true,
        iconPrefix: 'icon'
      }));

    // 执行处理
    return new Promise((resolve, reject) => {
      fontmin.run((err, files) => {
        if (err) {
          reject(err);
        } else {
          const result = this.processResults(files);
          resolve(result);
        }
      });
    });
  }

  // 处理结果
  processResults(files) {
    const result = {
      fonts: {},
      css: '',
      totalSize: 0,
      compressionStats: {}
    };

    files.forEach(file => {
      const ext = file.path.split('.').pop();
      const size = file.contents.length;
      
      result.totalSize += size;
      
      if (['ttf', 'woff', 'woff2', 'svg'].includes(ext)) {
        result.fonts[ext] = {
          path: file.path,
          size: size,
          sizeFormatted: this.formatFileSize(size)
        };
      } else if (ext === 'css') {
        result.css = file.contents.toString();
      }
    });

    // 计算压缩统计
    if (result.fonts.ttf && result.fonts.woff2) {
      const compressionRatio = (1 - result.fonts.woff2.size / result.fonts.ttf.size) * 100;
      result.compressionStats.woff2 = `${compressionRatio.toFixed(1)}%`;
    }

    return result;
  }

  // 格式化文件大小
  formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
}

// 使用示例
const processor = new FontminProcessor({
  srcPath: './src/fonts',
  destPath: './dist/fonts'
});

// 处理字体文件
async function processFont() {
  try {
    const characters = '这是一个测试字符串,包含了需要的所有字符。';
    
    const result = await processor.processAdvancedSubset('./src/fonts/SourceHanSans.ttf', {
      characters,
      fontFamily: 'SourceHanSans-Subset',
      removeHinting: true,
      optimizeLevel: 3
    });

    console.log('字体处理结果:', result);
  } catch (error) {
    console.error('字体处理失败:', error);
  }
}
示例:提取网页实际使用的字符并生成子集
// 网页字符提取和字体生成工具
class WebPageFontExtractor {
  constructor() {
    this.extractedChars = new Set();
    this.sourceAnalysis = {
      html: new Set(),
      css: new Set(),
      js: new Set(),
      json: new Set()
    };
  }

  // 分析整个项目的字符使用情况
  async analyzeProject(projectPath) {
    const analysis = {
      files: [],
      characters: new Set(),
      statistics: {},
      recommendations: []
    };

    try {
      // 递归分析项目文件
      await this.scanDirectory(projectPath, analysis);
      
      // 生成字符统计
      analysis.statistics = this.generateCharacterStatistics(analysis.characters);
      
      // 生成优化建议
      analysis.recommendations = this.generateRecommendations(analysis.statistics);
      
      return analysis;
    } catch (error) {
      console.error('项目分析失败:', error);
      throw error;
    }
  }

  // 扫描目录
  async scanDirectory(dirPath, analysis) {
    const fs = require('fs').promises;
    const path = require('path');
    
    try {
      const files = await fs.readdir(dirPath);
      
      for (const file of files) {
        const filePath = path.join(dirPath, file);
        const stat = await fs.stat(filePath);
        
        if (stat.isDirectory()) {
          // 跳过 node_modules 和其他忽略目录
          if (!['node_modules', '.git', 'dist', 'build'].includes(file)) {
            await this.scanDirectory(filePath, analysis);
          }
        } else {
          await this.analyzeFile(filePath, analysis);
        }
      }
    } catch (error) {
      console.warn(`扫描目录失败: ${dirPath}`, error);
    }
  }

  // 分析单个文件
  async analyzeFile(filePath, analysis) {
    const fs = require('fs').promises;
    const path = require('path');
    
    const ext = path.extname(filePath).toLowerCase();
    const supportedExtensions = ['.html', '.css', '.js', '.jsx', '.ts', '.tsx', '.json', '.md'];
    
    if (!supportedExtensions.includes(ext)) {
      return;
    }

    try {
      const content = await fs.readFile(filePath, 'utf-8');
      const fileAnalysis = {
        path: filePath,
        type: ext,
        size: content.length,
        characters: new Set()
      };

      // 根据文件类型进行不同的分析
      switch (ext) {
        case '.html':
          this.analyzeHTMLContent(content, fileAnalysis);
          break;
        case '.css':
          this.analyzeCSSContent(content, fileAnalysis);
          break;
        case '.js':
        case '.jsx':
        case '.ts':
        case '.tsx':
          this.analyzeJSContent(content, fileAnalysis);
          break;
        case '.json':
          this.analyzeJSONContent(content, fileAnalysis);
          break;
        case '.md':
          this.analyzeMarkdownContent(content, fileAnalysis);
          break;
      }

      // 合并字符到总分析中
      fileAnalysis.characters.forEach(char => {
        analysis.characters.add(char);
      });

      analysis.files.push(fileAnalysis);
    } catch (error) {
      console.warn(`文件分析失败: ${filePath}`, error);
    }
  }

  // 分析HTML内容
  analyzeHTMLContent(content, fileAnalysis) {
    // 移除HTML标签
    const textContent = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
                              .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
                              .replace(/<[^>]*>/g, '');
    
    // 解码HTML实体
    const decodedContent = this.decodeHTMLEntities(textContent);
    
    // 提取字符
    for (const char of decodedContent) {
      if (this.isValidCharacter(char)) {
        fileAnalysis.characters.add(char);
      }
    }

    // 分析属性值中的文本
    const attrMatches = content.match(/(?:title|alt|placeholder|value)=["']([^"']+)["']/gi) || [];
    attrMatches.forEach(match => {
      const value = match.match(/["']([^"']+)["']/)[1];
      for (const char of value) {
        if (this.isValidCharacter(char)) {
          fileAnalysis.characters.add(char);
        }
      }
    });
  }

  // 分析CSS内容
  analyzeCSSContent(content, fileAnalysis) {
    // 提取 content 属性的值
    const contentMatches = content.match(/content:\s*["']([^"']+)["']/gi) || [];
    contentMatches.forEach(match => {
      const value = match.match(/["']([^"']+)["']/)[1];
      for (const char of value) {
        if (this.isValidCharacter(char)) {
          fileAnalysis.characters.add(char);
        }
      }
    });

    // 提取字体族名称中的字符
    const fontFamilyMatches = content.match(/font-family:\s*["']([^"']+)["']/gi) || [];
    fontFamilyMatches.forEach(match => {
      const value = match.match(/["']([^"']+)["']/)[1];
      for (const char of value) {
        if (this.isValidCharacter(char)) {
          fileAnalysis.characters.add(char);
        }
      }
    });

    // 提取注释中的中文
    const commentMatches = content.match(/\/\*[\s\S]*?\*\//g) || [];
    commentMatches.forEach(comment => {
      for (const char of comment) {
        if (this.isValidCharacter(char) && this.isCJKCharacter(char)) {
          fileAnalysis.characters.add(char);
        }
      }
    });
  }

  // 分析JavaScript内容
  analyzeJSContent(content, fileAnalysis) {
    // 移除注释
    const cleanContent = content.replace(/\/\*[\s\S]*?\*\//g, '')
                               .replace(/\/\/.*$/gm, '');

    // 提取字符串字面量
    const stringMatches = cleanContent.match(/(['"`])(?:(?!\1)[^\\]|\\.))*?\1/g) || [];
    stringMatches.forEach(str => {
      // 移除引号
      const value = str.slice(1, -1);
      // 处理转义字符
      const unescaped = this.unescapeString(value);
      for (const char of unescaped) {
        if (this.isValidCharacter(char)) {
          fileAnalysis.characters.add(char);
        }
      }
    });

    // 提取模板字符串
    const templateMatches = cleanContent.match(/`[^`]*`/g) || [];
    templateMatches.forEach(template => {
      // 移除模板变量 ${...}
      const staticParts = template.replace(/\$\{[^}]*\}/g, '');
      for (const char of staticParts) {
        if (char !== '`' && this.isValidCharacter(char)) {
          fileAnalysis.characters.add(char);
        }
      }
    });
  }

  // 判断是否为有效字符
  isValidCharacter(char) {
    const code = char.charCodeAt(0);
    
    // 跳过控制字符(除了换行和制表符)
    if (code < 0x20 && code !== 0x09 && code !== 0x0A && code !== 0x0D) {
      return false;
    }
    
    // 跳过空白字符
    if (/\s/.test(char) && char !== ' ') {
      return false;
    }
    
    return true;
  }

  // 判断是否为CJK字符
  isCJKCharacter(char) {
    const code = char.charCodeAt(0);
    return (code >= 0x4E00 && code <= 0x9FFF) ||  // CJK统一汉字
           (code >= 0x3400 && code <= 0x4DBF) ||  // CJK扩展A
           (code >= 0x20000 && code <= 0x2A6DF) || // CJK扩展B
           (code >= 0x3040 && code <= 0x309F) ||  // 平假名
           (code >= 0x30A0 && code <= 0x30FF) ||  // 片假名
           (code >= 0xAC00 && code <= 0xD7AF);    // 韩文
  }

  // 生成字体子集
  async generateFontSubset(fontPath, characters, outputPath) {
    const Fontmin = require('fontmin');
    const uniqueChars = Array.from(new Set(characters)).join('');
    
    console.log(`开始生成字体子集...`);
    console.log(`原始字符数: ${characters.length}`);
    console.log(`去重后字符数: ${uniqueChars.length}`);
    
    const fontmin = new Fontmin()
      .src(fontPath)
      .dest(outputPath)
      .use(Fontmin.glyph({
        text: uniqueChars,
        hinting: false
      }))
      .use(Fontmin.ttf2woff2({
        clone: true
      }))
      .use(Fontmin.ttf2woff({
        clone: true
      }))
      .use(Fontmin.css({
        fontFamily: 'OptimizedFont',
        base64: false
      }));

    return new Promise((resolve, reject) => {
      fontmin.run((err, files) => {
        if (err) {
          reject(err);
        } else {
          console.log(`字体子集生成完成: ${files.length} 个文件`);
          resolve(files);
        }
      });
    });
  }

  // 生成使用报告
  generateUsageReport(analysis) {
    const report = {
      projectSummary: {
        totalFiles: analysis.files.length,
        totalCharacters: analysis.characters.size,
        fileTypes: {}
      },
      characterAnalysis: analysis.statistics,
      optimizationPlan: {
        estimatedSavings: this.calculateEstimatedSavings(analysis.statistics),
        recommendedSubsets: this.generateSubsetRecommendations(analysis.statistics),
        implementationSteps: this.generateImplementationSteps(analysis)
      }
    };

    // 文件类型统计
    analysis.files.forEach(file => {
      const type = file.type;
      if (!report.projectSummary.fileTypes[type]) {
        report.projectSummary.fileTypes[type] = 0;
      }
      report.projectSummary.fileTypes[type]++;
    });

    return report;
  }

  // 生成字符统计
  generateCharacterStatistics(characters) {
    const stats = {
      total: characters.size,
      byCategory: {
        latin: 0,
        chinese: 0,
        japanese: 0,
        korean: 0,
        numbers: 0,
        punctuation: 0,
        symbols: 0,
        whitespace: 0
      },
      frequencyAnalysis: {},
      recommendations: []
    };

    const charArray = Array.from(characters);
    
    // 分类统计
    charArray.forEach(char => {
      const code = char.charCodeAt(0);
      
      if (code >= 0x0030 && code <= 0x0039) {
        stats.byCategory.numbers++;
      } else if ((code >= 0x0041 && code <= 0x005A) || (code >= 0x0061 && code <= 0x007A)) {
        stats.byCategory.latin++;
      } else if (code >= 0x4E00 && code <= 0x9FFF) {
        stats.byCategory.chinese++;
      } else if ((code >= 0x3040 && code <= 0x309F) || (code >= 0x30A0 && code <= 0x30FF)) {
        stats.byCategory.japanese++;
      } else if (code >= 0xAC00 && code <= 0xD7AF) {
        stats.byCategory.korean++;
      } else if (/\s/.test(char)) {
        stats.byCategory.whitespace++;
      } else if (this.isPunctuation(char)) {
        stats.byCategory.punctuation++;
      } else {
        stats.byCategory.symbols++;
      }
    });

    return stats;
  }

  // 判断是否为标点符号
  isPunctuation(char) {
    const punctuationRanges = [
      [0x0020, 0x002F], // 基本标点
      [0x003A, 0x0040], // 冒号到@
      [0x005B, 0x0060], // [ 到 `
      [0x007B, 0x007E], // { 到 ~
      [0x3000, 0x303F], // CJK标点
      [0xFF00, 0xFFEF]  // 全角字符
    ];
    
    const code = char.charCodeAt(0);
    return punctuationRanges.some(([start, end]) => code >= start && code <= end);
  }
}

// 使用示例
const extractor = new WebPageFontExtractor();

// 分析项目并生成字体子集
async function optimizeProjectFonts(projectPath, fontPath, outputPath) {
  try {
    console.log('开始分析项目...');
    const analysis = await extractor.analyzeProject(projectPath);
    
    console.log('生成使用报告...');
    const report = extractor.generateUsageReport(analysis);
    console.log('项目分析完成:', report);
    
    console.log('生成字体子集...');
    const characters = Array.from(analysis.characters);
    const fontFiles = await extractor.generateFontSubset(fontPath, characters, outputPath);
    
    console.log('字体优化完成!');
    return {
      analysis,
      report,
      fontFiles
    };
  } catch (error) {
    console.error('字体优化失败:', error);
    throw error;
  }
}

进一步优化字体

示例:压缩字体文件并生成多种格式

// 高级字体压缩和格式转换工具
class AdvancedFontOptimizer {
  constructor(options = {}) {
    this.options = {
      compressionLevel: options.compressionLevel || 9,
      preserveHinting: options.preserveHinting || false,
      generateFormats: options.generateFormats || ['woff2', 'woff', 'ttf'],
      enableSubpixelOptimization: options.enableSubpixelOptimization || true,
      ...options
    };
  }

  // 综合优化流程
  async optimizeFont(fontPath, characters, options = {}) {
    const optimizationPlan = this.createOptimizationPlan(fontPath, characters, options);
    
    console.log('开始字体优化流程...');
    console.log('优化计划:', optimizationPlan);

    try {
      // 步骤1: 字符子集化
      const subsetResult = await this.performSubsetting(optimizationPlan);
      
      // 步骤2: 格式转换和压缩
      const formatResults = await this.performFormatConversion(subsetResult);
      
      // 步骤3: 进阶优化
      const advancedResults = await this.performAdvancedOptimization(formatResults);
      
      // 步骤4: 生成CSS和预加载提示
      const finalResults = await this.generateAssets(advancedResults);
      
      return this.createOptimizationReport(finalResults);
    } catch (error) {
      console.error('字体优化失败:', error);
      throw error;
    }
  }

  // 创建优化计划
  createOptimizationPlan(fontPath, characters, options) {
    const plan = {
      source: {
        path: fontPath,
        estimatedSize: 0 // 将在实际处理时填充
      },
      target: {
        characters: Array.from(new Set(characters)),
        formats: this.options.generateFormats,
        outputPath: options.outputPath || './dist/fonts'
      },
      optimization: {
        removeHinting: !this.options.preserveHinting,
        compressionLevel: this.options.compressionLevel,
        enableSubpixelOptimization: this.options.enableSubpixelOptimization,
        unicodeRangeOptimization: true
      },
      steps: [
        'subsetting',
        'format-conversion',
        'advanced-optimization',
        'asset-generation'
      ]
    };

    return plan;
  }

  // 执行字符子集化
  async performSubsetting(plan) {
    console.log('执行字符子集化...');
    
    const Fontmin = require('fontmin');
    const uniqueChars = plan.target.characters.join('');
    
    const fontmin = new Fontmin()
      .src(plan.source.path)
      .dest(plan.target.outputPath)
      .use(Fontmin.glyph({
        text: uniqueChars,
        hinting: !plan.optimization.removeHinting
      }));

    const files = await new Promise((resolve, reject) => {
      fontmin.run((err, files) => {
        if (err) reject(err);
        else resolve(files);
      });
    });

    return {
      ...plan,
      subsetFiles: files,
      stats: {
        originalSize: 0, // 需要从源文件获取
        subsetSize: files.reduce((total, file) => total + file.contents.length, 0),
        characterCount: uniqueChars.length
      }
    };
  }

  // 执行格式转换
  async performFormatConversion(subsetResult) {
    console.log('执行格式转换...');
    
    const conversions = [];
    const basePath = subsetResult.target.outputPath;
    
    for (const format of subsetResult.target.formats) {
      try {
        const result = await this.convertToFormat(subsetResult, format);
        conversions.push(result);
      } catch (error) {
        console.warn(`${format} 格式转换失败:`, error);
      }
    }

    return {
      ...subsetResult,
      conversions
    };
  }

  // 转换为特定格式
  async convertToFormat(subsetResult, format) {
    const Fontmin = require('fontmin');
    
    const fontmin = new Fontmin()
      .src(subsetResult.subsetFiles.filter(f => f.path.endsWith('.ttf'))[0].contents)
      .dest(subsetResult.target.outputPath);

    switch (format) {
      case 'woff2':
        fontmin.use(Fontmin.ttf2woff2({
          clone: true,
          deflate: true
        }));
        break;
      case 'woff':
        fontmin.use(Fontmin.ttf2woff({
          clone: true,
          deflate: true
        }));
        break;
      case 'eot':
        fontmin.use(Fontmin.ttf2eot({
          clone: true
        }));
        break;
      case 'svg':
        fontmin.use(Fontmin.ttf2svg({
          clone: true
        }));
        break;
    }

    const files = await new Promise((resolve, reject) => {
      fontmin.run((err, files) => {
        if (err) reject(err);
        else resolve(files);
      });
    });

    return {
      format,
      files,
      size: files.reduce((total, file) => total + file.contents.length, 0)
    };
  }

  // 执行高级优化
  async performAdvancedOptimization(formatResults) {
    console.log('执行高级优化...');
    
    const optimizedResults = [];
    
    for (const conversion of formatResults.conversions) {
      try {
        const optimized = await this.optimizeFormat(conversion);
        optimizedResults.push(optimized);
      } catch (error) {
        console.warn(`${conversion.format} 高级优化失败:`, error);
        optimizedResults.push(conversion); // 使用原始结果
      }
    }

    return {
      ...formatResults,
      optimizedResults
    };
  }

  // 优化特定格式
  async optimizeFormat(conversion) {
    // 根据格式应用特定优化
    switch (conversion.format) {
      case 'woff2':
        return await this.optimizeWOFF2(conversion);
      case 'woff':
        return await this.optimizeWOFF(conversion);
      case 'ttf':
        return await this.optimizeTTF(conversion);
      default:
        return conversion;
    }
  }

  // 优化WOFF2格式
  async optimizeWOFF2(conversion) {
    // WOFF2已经使用Brotli压缩,主要优化在于表结构
    console.log('优化WOFF2格式...');
    
    // 这里可以添加额外的WOFF2优化逻辑
    // 例如:移除不必要的表、优化字形数据等
    
    return {
      ...conversion,
      optimized: true,
      compressionRatio: this.calculateCompressionRatio(conversion)
    };
  }

  // 优化WOFF格式
  async optimizeWOFF(conversion) {
    console.log('优化WOFF格式...');
    
    // WOFF使用gzip压缩,可以调整压缩级别
    return {
      ...conversion,
      optimized: true,
      compressionRatio: this.calculateCompressionRatio(conversion)
    };
  }

  // 优化TTF格式
  async optimizeTTF(conversion) {
    console.log('优化TTF格式...');
    
    // TTF优化包括:移除不必要的表、优化轮廓数据等
    return {
      ...conversion,
      optimized: true,
      compressionRatio: this.calculateCompressionRatio(conversion)
    };
  }

  // 计算压缩比
  calculateCompressionRatio(conversion) {
    // 简化计算,实际需要与原始文件对比
    return '估算压缩比';
  }

  // 生成资源文件
  async generateAssets(optimizedResults) {
    console.log('生成资源文件...');
    
    const assets = {
      css: this.generateCSS(optimizedResults),
      preloadHints: this.generatePreloadHints(optimizedResults),
      fontDisplay: this.generateFontDisplayCSS(optimizedResults),
      fallbackCSS: this.generateFallbackCSS(optimizedResults)
    };

    return {
      ...optimizedResults,
      assets
    };
  }

  // 生成CSS文件
  generateCSS(results) {
    const formats = results.optimizedResults;
    let css = '/* 优化后的字体CSS */\n\n';
    
    // 主字体声明
    css += '@font-face {\n';
    css += '  font-family: "OptimizedFont";\n';
    css += '  font-style: normal;\n';
    css += '  font-weight: 400;\n';
    css += '  font-display: swap;\n';
    
    // 生成src属性
    const sources = [];
    
    const woff2 = formats.find(f => f.format === 'woff2');
    if (woff2) {
      sources.push(`url('OptimizedFont.woff2') format('woff2')`);
    }
    
    const woff = formats.find(f => f.format === 'woff');
    if (woff) {
      sources.push(`url('OptimizedFont.woff') format('woff')`);
    }
    
    const ttf = formats.find(f => f.format === 'ttf');
    if (ttf) {
      sources.push(`url('OptimizedFont.ttf') format('truetype')`);
    }
    
    css += `  src: ${sources.join(',\n       ')};\n`;
    
    // Unicode范围(如果可用)
    if (results.target.characters) {
      const unicodeRange = this.generateUnicodeRange(results.target.characters);
      css += `  unicode-range: ${unicodeRange};\n`;
    }
    
    css += '}\n\n';
    
    // 使用类
    css += '/* 使用优化字体的CSS类 */\n';
    css += '.font-optimized {\n';
    css += '  font-family: "OptimizedFont", sans-serif;\n';
    css += '}\n';

    return css;
  }

  // 生成Unicode范围
  generateUnicodeRange(characters) {
    // 简化实现,实际需要更复杂的范围计算
    const ranges = [];
    characters.slice(0, 10).forEach(char => {
      const code = char.charCodeAt(0);
      ranges.push(`U+${code.toString(16).toUpperCase().padStart(4, '0')}`);
    });
    
    return ranges.join(', ');
  }

  // 生成预加载提示
  generatePreloadHints(results) {
    const woff2 = results.optimizedResults.find(r => r.format === 'woff2');
    if (!woff2) return '';
    
    return `<link rel="preload" href="OptimizedFont.woff2" as="font" type="font/woff2" crossorigin>`;
  }

  // 生成字体显示CSS
  generateFontDisplayCSS(results) {
    return `
/* 字体显示优化 */
.font-loading {
  font-family: system-ui, -apple-system, sans-serif;
}

.font-loaded {
  font-family: "OptimizedFont", system-ui, -apple-system, sans-serif;
}

/* 字体加载动画 */
@keyframes fontFadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.font-loaded {
  animation: fontFadeIn 0.3s ease-in-out;
}
`;
  }

  // 生成降级CSS
  generateFallbackCSS(results) {
    return `
/* 字体降级策略 */
.font-fallback {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 
               "Helvetica Neue", Arial, "Noto Sans", sans-serif,
               "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 
               "Noto Color Emoji";
}

/* 中文字体降级 */
.font-fallback-zh {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
               "Source Han Sans CN", sans-serif;
}
`;
  }

  // 创建优化报告
  createOptimizationReport(finalResults) {
    const report = {
      summary: {
        originalCharacterCount: 'N/A', // 需要分析原始字体
        optimizedCharacterCount: finalResults.target.characters.length,
        generatedFormats: finalResults.optimizedResults.map(r => r.format),
        totalOptimizedSize: finalResults.optimizedResults.reduce((total, r) => total + r.size, 0)
      },
      formats: finalResults.optimizedResults.map(result => ({
        format: result.format,
        size: result.size,
        sizeFormatted: this.formatFileSize(result.size),
        optimized: result.optimized || false,
        compressionRatio: result.compressionRatio || 'N/A'
      })),
      assets: finalResults.assets,
      recommendations: this.generateOptimizationRecommendations(finalResults)
    };

    return report;
  }

  // 生成优化建议
  generateOptimizationRecommendations(results) {
    const recommendations = [];
    
    // 检查WOFF2支持
    const hasWOFF2 = results.optimizedResults.some(r => r.format === 'woff2');
    if (!hasWOFF2) {
      recommendations.push({
        type: 'format',
        priority: 'high',
        message: '建议生成WOFF2格式以获得最佳压缩率'
      });
    }
    
    // 检查字符数量
    if (results.target.characters.length > 1000) {
      recommendations.push({
        type: 'subset',
        priority: 'medium',
        message: '字符数量较多,考虑进一步细分字体子集'
      });
    }
    
    // 检查总文件大小
    const totalSize = results.optimizedResults.reduce((total, r) => total + r.size, 0);
    if (totalSize > 100 * 1024) { // 100KB
      recommendations.push({
        type: 'size',
        priority: 'medium',
        message: '总文件大小较大,建议检查字符使用的必要性'
      });
    }

    return recommendations;
  }

  // 格式化文件大小
  formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
}

// 使用示例
const optimizer = new AdvancedFontOptimizer({
  compressionLevel: 9,
  preserveHinting: false,
  generateFormats: ['woff2', 'woff', 'ttf'],
  enableSubpixelOptimization: true
});

// 执行高级优化
async function performAdvancedOptimization() {
  try {
    const characters = '这是需要优化的字符集合...';
    const fontPath = './src/fonts/SourceHanSans-Regular.ttf';
    
    const report = await optimizer.optimizeFont(fontPath, characters, {
      outputPath: './dist/fonts'
    });
    
    console.log('高级优化完成:', report);
    
    // 输出生成的CSS文件
    require('fs').writeFileSync('./dist/fonts/optimized-font.css', report.assets.css);
    
    return report;
  } catch (error) {
    console.error('高级优化失败:', error);
  }
}

在网页中使用优化后的字体

优化后的字体需要正确地集成到网页中,才能发挥最大的性能优势。

字体加载策略

graph TD
    A["字体加载策略"] --> B["预加载关键字体"]
    A --> C["渐进式增强"]
    A --> D["降级方案"]
    A --> E["性能监控"]
    
    B --> B1["Link preload"]
    B --> B2["Font-display: swap"]
    B --> B3["Critical font path"]
    
    C --> C1["基础字体先加载"]
    C --> C2["扩展字体后加载"]
    C --> C3["动态字体按需加载"]
    
    D --> D1["系统字体栈"]
    D --> D2["Web安全字体"]
    D --> D3["图标字体降级"]
    
    E --> E1["加载时间监控"]
    E --> E2["渲染性能追踪"]
    E --> E3["用户体验指标"]
    
    style A fill:#e3f2fd
    style B fill:#c8e6c9
    style C fill:#fff3e0
    style D fill:#ffecb3
    style E fill:#f3e5f5

完整的字体集成方案

// 字体加载和管理系统
class OptimizedFontManager {
  constructor(options = {}) {
    this.options = {
      fontBasePath: options.fontBasePath || '/fonts/',
      fallbackFonts: options.fallbackFonts || this.getDefaultFallbacks(),
      loadTimeout: options.loadTimeout || 3000,
      enableProgressiveLoading: options.enableProgressiveLoading !== false,
      enablePerformanceMonitoring: options.enablePerformanceMonitoring !== false,
      ...options
    };
    
    this.loadedFonts = new Set();
    this.loadingPromises = new Map();
    this.performanceMetrics = {
      loadStartTime: 0,
      loadEndTime: 0,
      fontsLoaded: 0,
      totalFonts: 0
    };
    
    this.init();
  }

  // 初始化字体管理器
  init() {
    this.setupFontDisplay();
    this.preloadCriticalFonts();
    
    if (this.options.enablePerformanceMonitoring) {
      this.setupPerformanceMonitoring();
    }
    
    // 页面加载完成后加载非关键字体
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => {
        this.loadSecondaryFonts();
      });
    } else {
      this.loadSecondaryFonts();
    }
  }

  // 设置字体显示策略
  setupFontDisplay() {
    // 添加字体加载状态类
    document.documentElement.classList.add('fonts-loading');
    
    // 设置默认字体栈
    const style = document.createElement('style');
    style.textContent = `
      /* 字体加载状态样式 */
      .fonts-loading {
        font-family: ${this.options.fallbackFonts.join(', ')};
      }
      
      .fonts-loaded {
        font-family: "OptimizedFont", ${this.options.fallbackFonts.join(', ')};
      }
      
      .fonts-failed {
        font-family: ${this.options.fallbackFonts.join(', ')};
      }
      
      /* 防止字体闪烁 */
      .font-swap {
        font-display: swap;
      }
      
      /* 字体加载动画 */
      @keyframes fontFadeIn {
        from { opacity: 0.8; }
        to { opacity: 1; }
      }
      
      .fonts-loaded {
        animation: fontFadeIn 0.2s ease-in-out;
      }
    `;
    document.head.appendChild(style);
  }

  // 预加载关键字体
  async preloadCriticalFonts() {
    console.log('预加载关键字体...');
    this.performanceMetrics.loadStartTime = performance.now();
    
    const criticalFonts = [
      {
        family: 'OptimizedFont-Core',
        url: `${this.options.fontBasePath}optimized-core.woff2`,
        format: 'woff2',
        priority: 'critical'
      }
    ];

    const preloadPromises = criticalFonts.map(font => this.preloadFont(font));
    
    try {
      await Promise.all(preloadPromises);
      console.log('关键字体预加载完成');
      this.onCriticalFontsLoaded();
    } catch (error) {
      console.error('关键字体预加载失败:', error);
      this.onFontLoadError();
    }
  }

  // 预加载单个字体
  async preloadFont(fontConfig) {
    return new Promise((resolve, reject) => {
      // 创建link preload
      const link = document.createElement('link');
      link.rel = 'preload';
      link.href = fontConfig.url;
      link.as = 'font';
      link.type = `font/${fontConfig.format}`;
      link.crossOrigin = 'anonymous';
      
      link.onload = () => {
        console.log(`字体预加载成功: ${fontConfig.family}`);
        resolve(fontConfig);
      };
      
      link.onerror = () => {
        console.error(`字体预加载失败: ${fontConfig.family}`);
        reject(new Error(`Failed to preload ${fontConfig.family}`));
      };
      
      document.head.appendChild(link);
      
      // 同时使用 Font Loading API
      this.loadFontWithAPI(fontConfig).then(resolve).catch(reject);
    });
  }

  // 使用 Font Loading API 加载字体
  async loadFontWithAPI(fontConfig) {
    if (!('fonts' in document)) {
      throw new Error('Font Loading API not supported');
    }

    const font = new FontFace(
      fontConfig.family,
      `url(${fontConfig.url}) format("${fontConfig.format}")`,
      {
        style: 'normal',
        weight: '400',
        display: 'swap'
      }
    );

    try {
      await font.load();
      document.fonts.add(font);
      this.loadedFonts.add(fontConfig.family);
      
      console.log(`Font Loading API 加载成功: ${fontConfig.family}`);
      return fontConfig;
    } catch (error) {
      console.error(`Font Loading API 加载失败: ${fontConfig.family}`, error);
      throw error;
    }
  }

  // 关键字体加载完成
  onCriticalFontsLoaded() {
    document.documentElement.classList.remove('fonts-loading');
    document.documentElement.classList.add('fonts-loaded');
    
    this.performanceMetrics.fontsLoaded++;
    
    // 触发自定义事件
    const event = new CustomEvent('criticalFontsLoaded', {
      detail: {
        loadTime: performance.now() - this.performanceMetrics.loadStartTime,
        fontsLoaded: Array.from(this.loadedFonts)
      }
    });
    document.dispatchEvent(event);
  }

  // 加载次要字体
  async loadSecondaryFonts() {
    if (!this.options.enableProgressiveLoading) {
      return;
    }

    console.log('开始加载次要字体...');
    
    const secondaryFonts = [
      {
        family: 'OptimizedFont-Extended',
        url: `${this.options.fontBasePath}optimized-extended.woff2`,
        format: 'woff2',
        priority: 'secondary'
      },
      {
        family: 'OptimizedFont-Icons',
        url: `${this.options.fontBasePath}optimized-icons.woff2`,
        format: 'woff2',
        priority: 'low'
      }
    ];

    // 延迟加载次要字体
    setTimeout(async () => {
      const loadPromises = secondaryFonts.map(font => 
        this.loadFontWithFallback(font)
      );
      
      try {
        const results = await Promise.allSettled(loadPromises);
        this.onSecondaryFontsLoaded(results);
      } catch (error) {
        console.error('次要字体加载失败:', error);
      }
    }, 100); // 延迟100ms确保关键渲染路径不被阻塞
  }

  // 带降级的字体加载
  async loadFontWithFallback(fontConfig) {
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error('Font load timeout')), this.options.loadTimeout);
    });

    try {
      const loadPromise = this.loadFontWithAPI(fontConfig);
      await Promise.race([loadPromise, timeoutPromise]);
      return fontConfig;
    } catch (error) {
      console.warn(`字体加载失败,使用降级方案: ${fontConfig.family}`, error);
      return null;
    }
  }

  // 次要字体加载完成
  onSecondaryFontsLoaded(results) {
    const successfulLoads = results.filter(r => r.status === 'fulfilled' && r.value);
    const failedLoads = results.filter(r => r.status === 'rejected' || !r.value);
    
    console.log(`次要字体加载完成: 成功 ${successfulLoads.length}, 失败 ${failedLoads.length}`);
    
    this.performanceMetrics.loadEndTime = performance.now();
    this.performanceMetrics.fontsLoaded = this.loadedFonts.size;
    
    // 触发完成事件
    const event = new CustomEvent('allFontsLoaded', {
      detail: {
        totalLoadTime: this.performanceMetrics.loadEndTime - this.performanceMetrics.loadStartTime,
        fontsLoaded: Array.from(this.loadedFonts),
        failedFonts: failedLoads.map(r => r.reason?.message || 'Unknown error')
      }
    });
    document.dispatchEvent(event);
  }

  // 字体加载错误处理
  onFontLoadError() {
    document.documentElement.classList.remove('fonts-loading');
    document.documentElement.classList.add('fonts-failed');
    
    console.warn('字体加载失败,使用系统字体降级');
    
    const event = new CustomEvent('fontLoadFailed', {
      detail: {
        fallbackFonts: this.options.fallbackFonts
      }
    });
    document.dispatchEvent(event);
  }

  // 设置性能监控
  setupPerformanceMonitoring() {
    // 监控字体相关的性能指标
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'resource' && entry.name.includes('.woff')) {
          console.log('字体资源加载性能:', {
            name: entry.name,
            duration: entry.duration,
            transferSize: entry.transferSize,
            encodedBodySize: entry.encodedBodySize
          });
        }
      }
    });
    
    observer.observe({ entryTypes: ['resource'] });

    // 监控字体相关的布局偏移
    if ('LayoutShift' in window) {
      const layoutObserver = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.hadRecentInput) continue;
          
          console.log('布局偏移检测:', {
            value: entry.value,
            sources: entry.sources?.map(s => s.node)
          });
        }
      });
      
      layoutObserver.observe({ entryTypes: ['layout-shift'] });
    }
  }

  // 获取默认降级字体
  getDefaultFallbacks() {
    return [
      'system-ui',
      '-apple-system',
      'BlinkMacSystemFont',
      '"Segoe UI"',
      'Roboto',
      '"Helvetica Neue"',
      'Arial',
      '"Noto Sans"',
      'sans-serif'
    ];
  }

  // 动态加载特定字体
  async loadFont(fontFamily, fontUrl, options = {}) {
    if (this.loadedFonts.has(fontFamily)) {
      return Promise.resolve();
    }

    if (this.loadingPromises.has(fontFamily)) {
      return this.loadingPromises.get(fontFamily);
    }

    const loadPromise = this.loadFontWithAPI({
      family: fontFamily,
      url: fontUrl,
      format: options.format || 'woff2',
      ...options
    });

    this.loadingPromises.set(fontFamily, loadPromise);

    try {
      await loadPromise;
      this.loadingPromises.delete(fontFamily);
      return Promise.resolve();
    } catch (error) {
      this.loadingPromises.delete(fontFamily);
      return Promise.reject(error);
    }
  }

  // 获取性能报告
  getPerformanceReport() {
    return {
      metrics: { ...this.performanceMetrics },
      loadedFonts: Array.from(this.loadedFonts),
      loadingStatus: this.loadingPromises.size > 0 ? 'loading' : 'complete',
      recommendations: this.generatePerformanceRecommendations()
    };
  }

  // 生成性能建议
  generatePerformanceRecommendations() {
    const recommendations = [];
    const totalLoadTime = this.performanceMetrics.loadEndTime - this.performanceMetrics.loadStartTime;
    
    if (totalLoadTime > 1000) {
      recommendations.push({
        type: 'performance',
        message: '字体加载时间较长,建议减少字体文件大小或字符数量',
        value: `${totalLoadTime.toFixed(2)}ms`
      });
    }
    
    if (this.loadedFonts.size > 5) {
      recommendations.push({
        type: 'optimization',
        message: '加载的字体数量较多,建议合并或减少字体变体',
        value: `${this.loadedFonts.size} 个字体`
      });
    }
    
    return recommendations;
  }

  // 清理资源
  destroy() {
    // 清理性能监控
    if (this.performanceObserver) {
      this.performanceObserver.disconnect();
    }
    
    // 清理加载中的Promise
    this.loadingPromises.clear();
    
    // 移除CSS类
    document.documentElement.classList.remove('fonts-loading', 'fonts-loaded', 'fonts-failed');
  }
}

// CSS样式文件生成器
class FontCSSGenerator {
  constructor(fontConfigs) {
    this.fontConfigs = fontConfigs;
  }

  // 生成完整的CSS文件
  generateCSS() {
    let css = '/* 优化字体 CSS 文件 */\n\n';
    
    // 生成@font-face声明
    css += this.generateFontFaces();
    
    // 生成使用类
    css += this.generateUtilityClasses();
    
    // 生成响应式字体
    css += this.generateResponsiveFonts();
    
    // 生成降级样式
    css += this.generateFallbackStyles();
    
    return css;
  }

  // 生成@font-face声明
  generateFontFaces() {
    let css = '/* Font Face 声明 */\n';
    
    this.fontConfigs.forEach(config => {
      css += `@font-face {\n`;
      css += `  font-family: "${config.family}";\n`;
      css += `  src: url("${config.woff2}") format("woff2"),\n`;
      css += `       url("${config.woff}") format("woff");\n`;
      css += `  font-weight: ${config.weight || '400'};\n`;
      css += `  font-style: ${config.style || 'normal'};\n`;
      css += `  font-display: swap;\n`;
      
      if (config.unicodeRange) {
        css += `  unicode-range: ${config.unicodeRange};\n`;
      }
      
      css += `}\n\n`;
    });
    
    return css;
  }

  // 生成工具类
  generateUtilityClasses() {
    return `
/* 字体工具类 */
.font-primary {
  font-family: "OptimizedFont-Core", var(--fallback-fonts);
}

.font-secondary {
  font-family: "OptimizedFont-Extended", var(--fallback-fonts);
}

.font-display {
  font-family: "OptimizedFont-Display", var(--fallback-fonts);
  font-weight: 700;
}

.font-mono {
  font-family: "OptimizedFont-Mono", "Courier New", monospace;
}

/* 字体大小 */
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }

/* 字体重量 */
.font-light { font-weight: 300; }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }

`;
  }

  // 生成响应式字体
  generateResponsiveFonts() {
    return `
/* 响应式字体 */
:root {
  --fallback-fonts: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

@media (max-width: 640px) {
  .text-responsive {
    font-size: clamp(0.875rem, 2.5vw, 1rem);
  }
  
  .heading-responsive {
    font-size: clamp(1.5rem, 5vw, 2.25rem);
  }
}

@media (min-width: 641px) {
  .text-responsive {
    font-size: clamp(1rem, 2vw, 1.125rem);
  }
  
  .heading-responsive {
    font-size: clamp(2.25rem, 4vw, 3rem);
  }
}

/* 高DPI屏幕优化 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  .font-primary {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
}

`;
  }

  // 生成降级样式
  generateFallbackStyles() {
    return `
/* 字体降级样式 */
.fonts-loading .font-primary,
.fonts-failed .font-primary {
  font-family: var(--fallback-fonts);
}

.fonts-loading .font-display,
.fonts-failed .font-display {
  font-family: var(--fallback-fonts);
  font-weight: 700;
}

/* 字体加载失败时的提示 */
.fonts-failed::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, #ff6b6b, #ffa726, #66bb6a);
  z-index: 9999;
  animation: fontLoadingIndicator 2s ease-in-out infinite;
}

@keyframes fontLoadingIndicator {
  0%, 100% { opacity: 0; }
  50% { opacity: 1; }
}

/* 打印样式 */
@media print {
  * {
    font-family: "Times New Roman", serif !important;
  }
}

`;
  }
}

// 使用示例
const fontManager = new OptimizedFontManager({
  fontBasePath: '/fonts/',
  enableProgressiveLoading: true,
  enablePerformanceMonitoring: true,
  loadTimeout: 3000
});

// 监听字体加载事件
document.addEventListener('criticalFontsLoaded', (event) => {
  console.log('关键字体加载完成:', event.detail);
});

document.addEventListener('allFontsLoaded', (event) => {
  console.log('所有字体加载完成:', event.detail);
  
  // 获取性能报告
  const report = fontManager.getPerformanceReport();
  console.log('字体性能报告:', report);
});

document.addEventListener('fontLoadFailed', (event) => {
  console.log('字体加载失败,已启用降级:', event.detail);
});

// 生成CSS文件
const fontConfigs = [
  {
    family: 'OptimizedFont-Core',
    woff2: '/fonts/optimized-core.woff2',
    woff: '/fonts/optimized-core.woff',
    weight: '400',
    unicodeRange: 'U+0020-007F, U+4E00-9FFF'
  },
  {
    family: 'OptimizedFont-Extended',
    woff2: '/fonts/optimized-extended.woff2',
    woff: '/fonts/optimized-extended.woff',
    weight: '400'
  }
];

const cssGenerator = new FontCSSGenerator(fontConfigs);
const generatedCSS = cssGenerator.generateCSS();

// 动态插入CSS或保存到文件
const styleElement = document.createElement('style');
styleElement.textContent = generatedCSS;
document.head.appendChild(styleElement);

HTML集成示例

<!DOCTYPE html>
<html lang="zh-CN" class="fonts-loading">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>字体子集化优化示例</title>
    
    <!-- 预加载关键字体 -->
    <link rel="preload" href="/fonts/optimized-core.woff2" as="font" type="font/woff2" crossorigin>
    
    <!-- 字体CSS -->
    <link rel="stylesheet" href="/css/fonts.css">
    
    <!-- 内联关键CSS -->
    <style>
        /* 关键字体样式 */
        .fonts-loading {
            font-family: system-ui, -apple-system, sans-serif;
        }
        
        .fonts-loaded {
            font-family: "OptimizedFont-Core", system-ui, -apple-system, sans-serif;
        }
        
        /* 防止布局偏移 */
        .title {
            font-size: 2rem;
            line-height: 1.2;
            margin: 0;
            min-height: 2.4rem; /* 预留空间防止偏移 */
        }
    </style>
</head>
<body>
    <header>
        <h1 class="title font-primary">字体子集化优化示例</h1>
        <p class="subtitle font-secondary">展示优化后的字体加载效果</p>
    </header>

    <main>
        <section>
            <h2 class="font-display">性能优化效果</h2>
            <p class="font-primary">
                通过字体子集化,我们将原始字体文件从 2MB 压缩到了 50KB,
                减少了 97.5% 的文件大小,显著提升了页面加载速度。
            </p>
        </section>

        <section>
            <h2 class="font-display">技术特点</h2>
            <ul class="font-primary">
                <li>智能字符提取</li>
                <li>多格式支持 (WOFF2, WOFF, TTF)</li>
                <li>渐进式加载</li>
                <li>降级方案</li>
                <li>性能监控</li>
            </ul>
        </section>
    </main>

    <!-- 字体管理脚本 -->
    <script src="/js/font-manager.js"></script>
    <script>
        // 初始化字体管理器
        const fontManager = new OptimizedFontManager({
            fontBasePath: '/fonts/',
            enableProgressiveLoading: true,
            enablePerformanceMonitoring: true
        });

        // 监听加载完成事件
        document.addEventListener('criticalFontsLoaded', () => {
            console.log('字体加载完成,页面渲染优化');
        });
    </script>
</body>
</html>

通过这个完整的字体子集化解决方案,您可以:

  1. 显著减少字体文件大小 - 通常可以减少 60-90% 的文件体积
  2. 提升页面加载性能 - 更快的字体下载和渲染
  3. 改善用户体验 - 减少字体闪烁和布局偏移
  4. 保持兼容性 - 完善的降级方案确保在各种环境下正常显示
  5. 实时性能监控 - 持续优化字体加载策略

这个系统化的方案将帮助您在实际项目中实现高效的字体优化,为用户提供更好的浏览体验。

为什么只在服务端使用?

字体子集化虽然是一项强大的优化技术,但它主要适用于服务端渲染(SSR)场景,而不是客户端动态处理。这种限制源于技术特性和实际应用需求的多重考量。

技术原理限制

字体子集化需要在构建时或请求时分析页面的完整内容,这个过程涉及复杂的字符提取和字体文件重构:

// 服务端字体子集化的核心原理
class ServerSideFontSubsetting {
  constructor() {
    this.buildTimeAnalysis = true;  // 构建时进行分析
    this.staticContentAnalysis = true;  // 静态内容可预测
    this.completePageContext = true;   // 拥有完整页面上下文
  }

  // 为什么适合服务端处理
  getServerSideAdvantages() {
    return {
      // 1. 完整的页面内容访问
      fullPageAccess: {
        description: '服务端可以访问完整的HTML、CSS、JavaScript内容',
        advantage: '能够准确分析所有可能使用的字符',
        example: `
          // 服务端可以分析所有模板文件
          const allTemplates = [
            './templates/header.html',
            './templates/footer.html', 
            './templates/content.html'
          ];
          
          const allCharacters = this.extractFromAllTemplates(allTemplates);
        `
      },

      // 2. 构建时优化
      buildTimeOptimization: {
        description: '在构建阶段就完成字体优化,无需运行时处理',
        advantage: '零运行时开销,最优性能',
        example: `
          // Webpack 构建时集成
          const FontSubsetPlugin = require('./font-subset-plugin');
          
          module.exports = {
            plugins: [
              new FontSubsetPlugin({
                fonts: ['./src/fonts/SourceHanSans.ttf'],
                buildTime: true,  // 构建时处理
                outputDir: './dist/fonts'
              })
            ]
          };
        `
      },

      // 3. 可预测的内容
      predictableContent: {
        description: 'SSR场景下内容是可预测的,不会动态变化',
        advantage: '可以生成最优化的字体子集',
        example: `
          // Next.js SSR 示例
          export async function getServerSideProps(context) {
            const pageContent = await getPageContent(context.params.id);
            
            // 服务端可以预先知道页面内容
            const requiredChars = extractCharacters(pageContent);
            const optimizedFont = await generateSubset(requiredChars);
            
            return {
              props: {
                content: pageContent,
                fontSubset: optimizedFont
              }
            };
          }
        `
      }
    };
  }
}
客户端处理的局限性

相比之下,客户端动态字体子集化面临诸多技术挑战:

// 客户端字体处理的挑战分析
class ClientSideLimitations {
  constructor() {
    this.performanceIssues = this.getPerformanceIssues();
    this.technicalConstraints = this.getTechnicalConstraints();
    this.userExperienceImpact = this.getUXImpact();
  }

  // 性能问题
  getPerformanceIssues() {
    return {
      // 1. 运行时字体处理开销
      runtimeProcessing: {
        problem: '客户端需要实时分析和处理字体',
        impact: '阻塞主线程,影响页面响应性',
        measurement: `
          // 性能测试代码
          const startTime = performance.now();
          
          // 客户端字体子集化处理
          const subset = await clientSideSubsetting(fontData, characters);
          
          const processingTime = performance.now() - startTime;
          console.log(\`客户端处理耗时: \${processingTime}ms\`);
          // 通常 > 500ms,严重影响用户体验
        `
      },

      // 2. 内存消耗
      memoryUsage: {
        problem: '需要在内存中加载和处理完整字体文件',
        impact: '移动设备内存压力大,可能导致页面崩溃',
        example: `
          // 内存使用情况
          const fontBuffer = new ArrayBuffer(2 * 1024 * 1024); // 2MB字体
          const processedFont = processFont(fontBuffer);
          
          // 内存峰值可能达到 4-6MB
          console.log('内存使用:', performance.memory?.usedJSHeapSize);
        `
      },

      // 3. 网络传输成本
      networkCost: {
        problem: '需要下载完整字体文件到客户端',
        impact: '浪费带宽,增加首次加载时间',
        comparison: `
          // 传输对比
          const fullFont = '2MB';      // 完整字体
          const subset = '50KB';       // 服务端子集
          const savingsRatio = '97.5%'; // 节省的带宽
          
          console.log(\`带宽节省: \${savingsRatio}\`);
        `
      }
    };
  }

  // 技术约束
  getTechnicalConstraints() {
    return {
      // 1. 浏览器兼容性
      browserCompatibility: {
        issue: '字体处理API在不同浏览器中支持程度不一',
        problems: [
          'Font Loading API 支持有限',
          'WebAssembly 字体处理库体积大',
          'Worker 线程支持不完整'
        ],
        example: `
          // 兼容性检测
          if (!('fonts' in document)) {
            console.warn('Font Loading API 不支持');
            fallbackToServerSide();
          }
          
          if (!window.WebAssembly) {
            console.warn('WebAssembly 不支持,无法进行复杂字体处理');
          }
        `
      },

      // 2. 安全限制
      securityConstraints: {
        issue: '浏览器安全策略限制字体文件的直接操作',
        problems: [
          'CORS 跨域限制',
          '字体文件解析安全风险',
          '第三方字体服务限制'
        ],
        example: `
          // CORS 问题
          fetch('/fonts/font.ttf')
            .then(response => response.arrayBuffer())
            .catch(error => {
              console.error('字体文件跨域访问被阻止:', error);
              // 必须回退到服务端处理
            });
        `
      }
    };
  }

  // 用户体验影响
  getUXImpact() {
    return {
      // 1. 字体闪烁 (FOIT/FOUT)
      fontFlashing: {
        description: '客户端处理期间会出现字体闪烁',
        impact: '严重影响视觉体验和可读性',
        solution: '服务端预处理可以避免这个问题'
      },

      // 2. 布局偏移 (CLS)
      layoutShift: {
        description: '动态字体切换导致布局不稳定',
        impact: '影响 Core Web Vitals 指标',
        measurement: `
          // CLS 监控
          new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
              if (entry.hadRecentInput) continue;
              console.log('布局偏移值:', entry.value);
              // 客户端字体处理常导致 CLS > 0.1
            }
          }).observe({entryTypes: ['layout-shift']});
        `
      }
    };
  }
}
服务端渲染的最佳实践

基于这些技术分析,服务端渲染成为字体子集化的理想场景:

// 服务端字体子集化的完整实现
class SSRFontOptimization {
  constructor(options = {}) {
    this.buildConfig = options.buildConfig || {};
    this.cacheStrategy = options.cacheStrategy || 'aggressive';
    this.fontProcessingQueue = new Map();
  }

  // 构建时字体处理流程
  async buildTimeFontProcessing(projectConfig) {
    console.log('开始构建时字体优化...');
    
    try {
      // 1. 分析所有页面内容
      const allPages = await this.scanAllPages(projectConfig.pagesDir);
      
      // 2. 提取字符集
      const characterSets = await this.extractCharacterSets(allPages);
      
      // 3. 生成字体子集
      const fontSubsets = await this.generateFontSubsets(characterSets);
      
      // 4. 优化和压缩
      const optimizedFonts = await this.optimizeFontFiles(fontSubsets);
      
      // 5. 生成静态资源
      const staticAssets = await this.generateStaticAssets(optimizedFonts);
      
      return {
        fonts: optimizedFonts,
        assets: staticAssets,
        performance: this.calculatePerformanceGains(optimizedFonts)
      };
    } catch (error) {
      console.error('构建时字体处理失败:', error);
      throw error;
    }
  }

  // 扫描所有页面
  async scanAllPages(pagesDir) {
    const fs = require('fs').promises;
    const path = require('path');
    const glob = require('glob');
    
    const pageFiles = glob.sync('**/*.{html,jsx,tsx,vue}', {
      cwd: pagesDir,
      absolute: true
    });
    
    const pages = [];
    for (const file of pageFiles) {
      const content = await fs.readFile(file, 'utf-8');
      pages.push({
        path: file,
        content,
        type: path.extname(file),
        size: content.length
      });
    }
    
    return pages;
  }

  // 提取字符集
  async extractCharacterSets(pages) {
    const characterSets = {
      core: new Set(),      // 核心字符
      extended: new Set(),  // 扩展字符
      rare: new Set()       // 罕见字符
    };
    
    for (const page of pages) {
      const chars = this.extractCharsFromContent(page.content, page.type);
      
      chars.forEach(char => {
        const frequency = this.getCharacterFrequency(char);
        
        if (frequency > 0.8) {
          characterSets.core.add(char);
        } else if (frequency > 0.3) {
          characterSets.extended.add(char);
        } else {
          characterSets.rare.add(char);
        }
      });
    }
    
    return {
      core: Array.from(characterSets.core),
      extended: Array.from(characterSets.extended),
      rare: Array.from(characterSets.rare),
      total: characterSets.core.size + characterSets.extended.size + characterSets.rare.size
    };
  }

  // 生成字体子集
  async generateFontSubsets(characterSets) {
    const Fontmin = require('fontmin');
    const subsets = {};
    
    // 生成核心字体子集
    subsets.core = await this.createSubset('core', characterSets.core, {
      priority: 'critical',
      formats: ['woff2', 'woff'],
      optimization: 'aggressive'
    });
    
    // 生成扩展字体子集
    subsets.extended = await this.createSubset('extended', characterSets.extended, {
      priority: 'secondary',
      formats: ['woff2'],
      optimization: 'standard'
    });
    
    // 罕见字符使用降级方案
    subsets.fallback = await this.createFallbackStrategy(characterSets.rare);
    
    return subsets;
  }

  // 创建单个子集
  async createSubset(name, characters, options) {
    const fontmin = new Fontmin()
      .src('./src/fonts/source.ttf')
      .dest(`./dist/fonts/${name}/`)
      .use(Fontmin.glyph({
        text: characters.join(''),
        hinting: false
      }));
    
    // 根据选项添加格式转换
    if (options.formats.includes('woff2')) {
      fontmin.use(Fontmin.ttf2woff2({ clone: true }));
    }
    
    if (options.formats.includes('woff')) {
      fontmin.use(Fontmin.ttf2woff({ clone: true }));
    }
    
    const files = await new Promise((resolve, reject) => {
      fontmin.run((err, files) => err ? reject(err) : resolve(files));
    });
    
    return {
      name,
      files,
      characters: characters.length,
      priority: options.priority,
      totalSize: files.reduce((sum, file) => sum + file.contents.length, 0)
    };
  }

  // 计算性能收益
  calculatePerformanceGains(optimizedFonts) {
    const originalSize = 2 * 1024 * 1024; // 假设原始字体 2MB
    const optimizedSize = Object.values(optimizedFonts)
      .reduce((sum, subset) => sum + subset.totalSize, 0);
    
    const savings = originalSize - optimizedSize;
    const savingsPercent = (savings / originalSize * 100).toFixed(2);
    
    return {
      originalSize: this.formatBytes(originalSize),
      optimizedSize: this.formatBytes(optimizedSize),
      savings: this.formatBytes(savings),
      savingsPercent: `${savingsPercent}%`,
      estimatedLoadTimeImprovement: `${(savings / 50000).toFixed(1)}s`, // 假设 50KB/s
      bandwidthSavings: this.formatBytes(savings * 1000) // 假设1000次访问
    };
  }

  // 格式化字节数
  formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
}

// Next.js 集成示例
class NextJSFontIntegration {
  static getNextConfig() {
    return {
      // next.config.js
      webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
        if (!dev && isServer) {
          // 只在生产构建的服务端处理字体
          config.plugins.push(
            new (require('./plugins/font-subset-plugin'))({
              srcDir: './src',
              fontDir: './src/fonts',
              outputDir: './public/fonts',
              enableBuildTimeOptimization: true
            })
          );
        }
        return config;
      },
      
      // 字体预加载配置
      async headers() {
        return [
          {
            source: '/fonts/:path*',
            headers: [
              {
                key: 'Cache-Control',
                value: 'public, max-age=31536000, immutable'
              }
            ]
          }
        ];
      }
    };
  }

  // 页面级字体优化
  static getServerSideProps() {
    return async (context) => {
      // 服务端可以根据页面内容动态选择字体子集
      const pageType = context.params.type;
      const fontSubset = this.selectOptimalFontSubset(pageType);
      
      return {
        props: {
          fontSubset,
          preloadHints: fontSubset.critical
        }
      };
    };
  }
}

// 使用示例
const ssrOptimizer = new SSRFontOptimization({
  buildConfig: {
    sourceDir: './src',
    outputDir: './dist',
    fontFormats: ['woff2', 'woff']
  },
  cacheStrategy: 'aggressive'
});

// 构建时执行
async function buildTimeOptimization() {
  try {
    const result = await ssrOptimizer.buildTimeFontProcessing({
      pagesDir: './src/pages'
    });
    
    console.log('字体优化完成:', result.performance);
    console.log('生成的字体文件:', result.fonts);
  } catch (error) {
    console.error('字体优化失败:', error);
  }
}
总结:服务端优势的核心原因

1. 技术可行性

  • ✅ 完整页面内容访问权限
  • ✅ 构建时处理,零运行时开销
  • ✅ 成熟的服务端字体处理工具链

2. 性能优势

  • ✅ 预先优化,无客户端处理延迟
  • ✅ 最小化网络传输
  • ✅ 避免字体闪烁和布局偏移

3. 用户体验

  • ✅ 更快的首次加载时间
  • ✅ 稳定的渲染表现
  • ✅ 更好的 Core Web Vitals 指标

4. 维护成本

  • ✅ 构建时集成,开发体验佳
  • ✅ 版本控制友好
  • ✅ 易于调试和优化

因此,虽然理论上客户端也可以进行字体子集化,但考虑到技术实现的复杂性、性能成本和用户体验,服务端渲染环境是字体子集化的最佳应用场景。这种设计选择确保了技术方案的实用性和可维护性,为用户提供最优的性能表现。