字体子集化
字体子集化是现代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>
通过这个完整的字体子集化解决方案,您可以:
- 显著减少字体文件大小 - 通常可以减少 60-90% 的文件体积
- 提升页面加载性能 - 更快的字体下载和渲染
- 改善用户体验 - 减少字体闪烁和布局偏移
- 保持兼容性 - 完善的降级方案确保在各种环境下正常显示
- 实时性能监控 - 持续优化字体加载策略
这个系统化的方案将帮助您在实际项目中实现高效的字体优化,为用户提供更好的浏览体验。
为什么只在服务端使用?
字体子集化虽然是一项强大的优化技术,但它主要适用于服务端渲染(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. 维护成本
- ✅ 构建时集成,开发体验佳
- ✅ 版本控制友好
- ✅ 易于调试和优化
因此,虽然理论上客户端也可以进行字体子集化,但考虑到技术实现的复杂性、性能成本和用户体验,服务端渲染环境是字体子集化的最佳应用场景。这种设计选择确保了技术方案的实用性和可维护性,为用户提供最优的性能表现。