跳到主要内容

字体优化技术详解

概述

字体优化是前端性能优化的关键技术,通过合理的字体加载策略、子集化和格式选择来减少字体文件大小,提升页面加载速度,避免字体闪烁,改善用户体验。

字体优化原理

字体加载过程

字体加载过程图
┌─────────────────────────────────────────────────────────────┐
│ 字体加载阶段 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 阻塞期 │ │ 交换期 │ │ 失效期 │ │ 回退期 │ │
│ │ 显示 │ │ 字体 │ │ 字体 │ │ 使用 │ │
│ │ 系统字体│ │ 加载 │ │ 替换 │ │ 回退字体│ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 字体显示策略 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ auto │ │ block │ │ swap │ │ fallback│ │
│ │ 默认 │ │ 阻塞 │ │ 交换 │ │ 回退 │ │
│ │ 行为 │ │ 显示 │ │ 显示 │ │ 显示 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘

字体格式对比

字体格式性能对比
┌─────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ 格式 │ 压缩率 │ 浏览器支持 │ 加载速度 │ 文件大小 │ 兼容性 │
├─────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ TTF │ 低 │ 良好 │ 慢 │ 大 │ 中等 │
│ OTF │ 低 │ 良好 │ 慢 │ 大 │ 中等 │
│ WOFF │ 中等 │ 优秀 │ 中等 │ 中等 │ 优秀 │
│ WOFF2 │ 高 │ 良好 │ 快 │ 小 │ 良好 │
│ EOT │ 低 │ 差 │ 慢 │ 大 │ 差 │
└─────────┴──────────┴──────────┴──────────┴──────────┴──────────┘

字体加载策略

font-display属性

/* 字体显示策略 */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2'),
url('font.woff') format('woff');
font-weight: 400;
font-style: normal;

/* 字体显示策略 */
font-display: swap; /* 优先显示系统字体,避免闪烁 */
}

/* 不同策略的效果 */
.font-auto {
font-display: auto; /* 浏览器默认行为 */
}

.font-block {
font-display: block; /* 阻塞显示,直到字体加载完成 */
}

.font-swap {
font-display: swap; /* 立即显示回退字体,字体加载完成后替换 */
}

.font-fallback {
font-display: fallback; /* 短暂阻塞后回退,避免频繁切换 */
}

.font-optional {
font-display: optional; /* 可选字体,网络慢时不加载 */
}

字体预加载

<!-- HTML字体预加载 -->
<head>
<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

<!-- 预连接到字体服务 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- 预取可能需要的字体 -->
<link rel="prefetch" href="/fonts/icon.woff2">
</head>
// JavaScript字体预加载器
class FontPreloader {
constructor() {
this.preloadedFonts = new Set();
this.fontObservers = new Map();
}

// 预加载字体
preloadFont(fontUrl, options = {}) {
if (this.preloadedFonts.has(fontUrl)) return;

const {
as = 'font',
type = 'font/woff2',
crossorigin = true,
onload = null,
onerror = null
} = options;

const link = document.createElement('link');
link.rel = 'preload';
link.href = fontUrl;
link.as = as;
link.type = type;

if (crossorigin) {
link.crossOrigin = 'anonymous';
}

if (onload) link.onload = onload;
if (onerror) link.onerror = onerror;

document.head.appendChild(link);
this.preloadedFonts.add(fontUrl);

console.log(`字体预加载: ${fontUrl}`);
}

// 预加载多个字体
preloadMultiple(fonts) {
fonts.forEach(font => {
if (typeof font === 'string') {
this.preloadFont(font);
} else {
this.preloadFont(font.url, font.options);
}
});
}

// 预连接到字体服务
preconnectFontService(domain) {
const link = document.createElement('link');
link.rel = 'preconnect';
link.href = domain;
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
}

// 检查字体是否已加载
isFontLoaded(fontFamily) {
return document.fonts.check(`12px "${fontFamily}"`);
}

// 等待字体加载完成
waitForFont(fontFamily, timeout = 5000) {
return new Promise((resolve, reject) => {
if (this.isFontLoaded(fontFamily)) {
resolve();
return;
}

const timer = setTimeout(() => {
reject(new Error(`字体加载超时: ${fontFamily}`));
}, timeout);

document.fonts.ready.then(() => {
clearTimeout(timer);
if (this.isFontLoaded(fontFamily)) {
resolve();
} else {
reject(new Error(`字体加载失败: ${fontFamily}`));
}
});
});
}

// 字体加载状态监控
monitorFontLoading(fontFamily) {
return new Promise((resolve) => {
if (this.isFontLoaded(fontFamily)) {
resolve('loaded');
return;
}

const observer = new FontFaceObserver(fontFamily);
observer.load().then(() => {
resolve('loaded');
}).catch(() => {
resolve('failed');
});
});
}
}

// 使用字体预加载器
const fontPreloader = new FontPreloader();

// 预加载关键字体
fontPreloader.preloadMultiple([
'/fonts/main.woff2',
'/fonts/bold.woff2',
'/fonts/icon.woff2'
]);

// 预连接Google Fonts
fontPreloader.preconnectFontService('https://fonts.googleapis.com');
fontPreloader.preconnectFontService('https://fonts.gstatic.com');

字体子集化

字体子集生成

// 字体子集生成器
class FontSubsetGenerator {
constructor() {
this.fontkit = null;
this.initFontkit();
}

// 初始化Fontkit
async initFontkit() {
try {
this.fontkit = await import('fontkit');
} catch (error) {
console.warn('Fontkit不可用,使用备用方案');
}
}

// 生成字体子集
async generateSubset(fontPath, text, outputPath) {
if (!this.fontkit) {
throw new Error('Fontkit未初始化');
}

try {
const font = await this.fontkit.open(fontPath);

// 获取文本中使用的字符
const usedChars = this.extractUsedCharacters(text);

// 创建子集字体
const subset = font.createSubset(usedChars);
const buffer = await subset.encode();

// 保存子集字体
await this.saveFontFile(buffer, outputPath);

console.log(`字体子集生成完成: ${outputPath}`);
console.log(`包含字符数: ${usedChars.length}`);

return {
success: true,
charCount: usedChars.length,
fileSize: buffer.length
};
} catch (error) {
console.error('字体子集生成失败:', error);
throw error;
}
}

// 提取使用的字符
extractUsedCharacters(text) {
const chars = new Set();

// 添加基本ASCII字符
for (let i = 32; i <= 126; i++) {
chars.add(String.fromCharCode(i));
}

// 添加文本中的字符
for (const char of text) {
chars.add(char);
}

// 添加常用标点符号
const punctuation = ',。!?;:""''()【】《》…—';
for (const char of punctuation) {
chars.add(char);
}

return Array.from(chars).sort();
}

// 保存字体文件
async saveFontFile(buffer, outputPath) {
// 在浏览器环境中,这里需要下载文件
const blob = new Blob([buffer], { type: 'font/woff2' });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = outputPath.split('/').pop();
link.click();

URL.revokeObjectURL(url);
}

// 分析字体使用情况
async analyzeFontUsage(fontPath, text) {
if (!this.fontkit) {
throw new Error('Fontkit未初始化');
}

const font = await this.fontkit.open(fontPath);
const usedChars = this.extractUsedCharacters(text);
const totalChars = font.numGlyphs;

return {
totalCharacters: totalChars,
usedCharacters: usedChars.length,
usageRatio: (usedChars.length / totalChars * 100).toFixed(2) + '%',
potentialSavings: totalChars - usedChars.length
};
}

// 生成多语言子集
async generateMultilingualSubset(fontPath, languages, outputPath) {
const allText = languages.map(lang => lang.text).join('');
return this.generateSubset(fontPath, allText, outputPath);
}
}

// 使用字体子集生成器
const subsetGenerator = new FontSubsetGenerator();

// 生成中文字体子集
async function generateChineseSubset() {
const chineseText = `
你好世界,欢迎使用字体子集化工具。
这个工具可以帮助你减少字体文件大小,
提升网页加载速度。
`;

try {
const result = await subsetGenerator.generateSubset(
'./fonts/NotoSansSC-Regular.otf',
chineseText,
'./fonts/subset.woff2'
);

console.log('子集生成结果:', result);
} catch (error) {
console.error('子集生成失败:', error);
}
}

// 分析字体使用情况
async function analyzeFontUsage() {
try {
const analysis = await subsetGenerator.analyzeFontUsage(
'./fonts/NotoSansSC-Regular.otf',
'你好世界Hello World'
);

console.log('字体使用分析:', analysis);
} catch (error) {
console.error('分析失败:', error);
}
}

动态字体子集

// 动态字体子集管理器
class DynamicFontSubsetManager {
constructor() {
this.subsets = new Map();
this.currentSubset = null;
this.observer = null;
}

// 初始化
async init() {
// 监听页面内容变化
this.observeContentChanges();

// 生成初始子集
await this.generateInitialSubset();
}

// 生成初始子集
async generateInitialSubset() {
const initialText = this.extractPageText();
await this.createSubset(initialText, 'initial');
}

// 提取页面文本
extractPageText() {
const textNodes = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, span, div');
let text = '';

textNodes.forEach(node => {
if (node.textContent) {
text += node.textContent + ' ';
}
});

return text.trim();
}

// 创建字体子集
async createSubset(text, name) {
try {
// 这里应该调用字体子集生成API
const subset = await this.generateSubsetAPI(text);

this.subsets.set(name, {
text,
subset,
timestamp: Date.now()
});

console.log(`字体子集创建成功: ${name}`);
return subset;
} catch (error) {
console.error(`字体子集创建失败: ${name}`, error);
throw error;
}
}

// 调用字体子集生成API
async generateSubsetAPI(text) {
// 模拟API调用
return new Promise((resolve) => {
setTimeout(() => {
resolve({
url: `/api/font-subset?text=${encodeURIComponent(text)}`,
size: Math.floor(Math.random() * 100) + 50
});
}, 1000);
});
}

// 监听内容变化
observeContentChanges() {
this.observer = new MutationObserver((mutations) => {
let hasTextChanges = false;

mutations.forEach(mutation => {
if (mutation.type === 'childList' || mutation.type === 'characterData') {
hasTextChanges = true;
}
});

if (hasTextChanges) {
this.handleContentChange();
}
});

this.observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
}

// 处理内容变化
async handleContentChange() {
const newText = this.extractPageText();
const currentSubset = this.subsets.get('current');

if (!currentSubset || this.hasSignificantChanges(currentSubset.text, newText)) {
// 创建新的子集
await this.createSubset(newText, 'current');
this.updateFontFace();
}
}

// 检查是否有显著变化
hasSignificantChanges(oldText, newText) {
const oldChars = new Set(oldText);
const newChars = new Set(newText);

// 计算新增字符数
let newCharCount = 0;
for (const char of newChars) {
if (!oldChars.has(char)) {
newCharCount++;
}
}

// 如果新增字符超过阈值,认为有显著变化
return newCharCount > 10;
}

// 更新字体
updateFontFace() {
const currentSubset = this.subsets.get('current');
if (!currentSubset) return;

// 移除旧的字体定义
const oldFontFace = document.querySelector('#dynamic-font-face');
if (oldFontFace) {
oldFontFace.remove();
}

// 添加新的字体定义
const style = document.createElement('style');
style.id = 'dynamic-font-face';
style.textContent = `
@font-face {
font-family: 'DynamicFont';
src: url('${currentSubset.subset.url}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
`;

document.head.appendChild(style);
}

// 销毁
destroy() {
if (this.observer) {
this.observer.disconnect();
}
this.subsets.clear();
}
}

// 使用动态字体子集管理器
const dynamicFontManager = new DynamicFontSubsetManager();

// 初始化
document.addEventListener('DOMContentLoaded', () => {
dynamicFontManager.init();
});

字体加载优化

字体加载状态管理

// 字体加载状态管理器
class FontLoadingManager {
constructor() {
this.fonts = new Map();
this.loadingPromises = new Map();
this.loadingStates = new Map();
}

// 注册字体
registerFont(fontFamily, fontUrls) {
this.fonts.set(fontFamily, fontUrls);
this.loadingStates.set(fontFamily, 'pending');
}

// 加载字体
async loadFont(fontFamily) {
if (this.loadingStates.get(fontFamily) === 'loaded') {
return Promise.resolve();
}

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

const loadPromise = this.loadFontFiles(fontFamily);
this.loadingPromises.set(fontFamily, loadPromise);

try {
await loadPromise;
this.loadingStates.set(fontFamily, 'loaded');
console.log(`字体加载完成: ${fontFamily}`);
} catch (error) {
this.loadingStates.set(fontFamily, 'failed');
console.error(`字体加载失败: ${fontFamily}`, error);
}

return loadPromise;
}

// 加载字体文件
async loadFontFiles(fontFamily) {
const fontUrls = this.fonts.get(fontFamily);
if (!fontUrls) {
throw new Error(`字体未注册: ${fontFamily}`);
}

const loadPromises = fontUrls.map(url => this.loadFontFile(url));
await Promise.all(loadPromises);
}

// 加载单个字体文件
loadFontFile(url) {
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;

link.onload = resolve;
link.onerror = reject;

document.head.appendChild(link);
});
}

// 检查字体加载状态
getFontStatus(fontFamily) {
return this.loadingStates.get(fontFamily) || 'unknown';
}

// 等待字体加载完成
waitForFont(fontFamily, timeout = 10000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`字体加载超时: ${fontFamily}`));
}, timeout);

const checkStatus = () => {
const status = this.getFontStatus(fontFamily);
if (status === 'loaded') {
clearTimeout(timer);
resolve();
} else if (status === 'failed') {
clearTimeout(timer);
reject(new Error(`字体加载失败: ${fontFamily}`));
} else {
setTimeout(checkStatus, 100);
}
};

checkStatus();
});
}

// 预加载字体
preloadFont(fontFamily) {
const fontUrls = this.fonts.get(fontFamily);
if (!fontUrls) return;

fontUrls.forEach(url => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = url;
link.as = 'font';
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
});
}

// 获取所有字体状态
getAllFontStatuses() {
const statuses = {};
this.fonts.forEach((urls, family) => {
statuses[family] = this.getFontStatus(family);
});
return statuses;
}
}

// 使用字体加载管理器
const fontManager = new FontLoadingManager();

// 注册字体
fontManager.registerFont('CustomFont', [
'/fonts/CustomFont-Regular.woff2',
'/fonts/CustomFont-Bold.woff2'
]);

// 预加载字体
fontManager.preloadFont('CustomFont');

// 加载字体
fontManager.loadFont('CustomFont').then(() => {
console.log('字体加载完成');
}).catch(error => {
console.error('字体加载失败:', error);
});

字体回退策略

/* 字体回退策略 */
.font-stack {
/* 主字体 → 备用字体 → 系统字体 */
font-family: 'CustomFont', 'Arial', 'Helvetica', sans-serif;
}

/* 针对不同语言的字体回退 */
.font-chinese {
font-family: 'NotoSansSC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}

.font-japanese {
font-family: 'NotoSansJP', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Meiryo', sans-serif;
}

.font-korean {
font-family: 'NotoSansKR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}

/* 字体回退测试 */
.font-fallback-test {
font-family: 'NonExistentFont', 'Arial', sans-serif;
/* 如果主字体不存在,自动回退到Arial */
}
// 字体回退测试器
class FontFallbackTester {
constructor() {
this.testString = 'The quick brown fox jumps over the lazy dog 你好世界';
this.testElement = null;
this.createTestElement();
}

// 创建测试元素
createTestElement() {
this.testElement = document.createElement('div');
this.testElement.style.cssText = `
position: absolute;
left: -9999px;
top: -9999px;
visibility: hidden;
font-size: 72px;
white-space: nowrap;
`;
this.testElement.textContent = this.testString;
document.body.appendChild(this.testElement);
}

// 测试字体是否可用
testFont(fontFamily) {
this.testElement.style.fontFamily = fontFamily;

// 测量文本宽度
const width = this.testElement.offsetWidth;

// 如果宽度为0,说明字体不可用
return width > 0;
}

// 测试字体回退
testFontFallback(fontStack) {
const fonts = fontStack.split(',').map(f => f.trim().replace(/['"]/g, ''));

for (const font of fonts) {
if (this.testFont(font)) {
return {
available: true,
font: font,
index: fonts.indexOf(font)
};
}
}

return {
available: false,
font: null,
index: -1
};
}

// 测试多个字体栈
testMultipleFontStacks(fontStacks) {
const results = {};

fontStacks.forEach(stack => {
results[stack] = this.testFontFallback(stack);
});

return results;
}

// 生成字体回退建议
generateFallbackSuggestions(primaryFont) {
const suggestions = {
'sans-serif': `${primaryFont}, Arial, Helvetica, sans-serif`,
'serif': `${primaryFont}, Times, 'Times New Roman', serif`,
'monospace': `${primaryFont}, 'Courier New', Courier, monospace`,
'chinese': `${primaryFont}, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif`,
'japanese': `${primaryFont}, 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Meiryo', sans-serif`
};

return suggestions;
}

// 销毁测试元素
destroy() {
if (this.testElement) {
document.body.removeChild(this.testElement);
this.testElement = null;
}
}
}

// 使用字体回退测试器
const fallbackTester = new FontFallbackTester();

// 测试字体回退
const testResults = fallbackTester.testMultipleFontStacks([
'CustomFont, Arial, sans-serif',
'NonExistentFont, Helvetica, sans-serif',
'NotoSansSC, PingFang SC, sans-serif'
]);

console.log('字体回退测试结果:', testResults);

// 生成字体回退建议
const suggestions = fallbackTester.generateFallbackSuggestions('CustomFont');
console.log('字体回退建议:', suggestions);

// 清理
fallbackTester.destroy();

最佳实践

1. 字体格式选择

  • 现代浏览器: WOFF2优先,WOFF备选
  • 兼容性要求: WOFF + TTF/OTF
  • 移动端: WOFF2 + 系统字体回退

2. 字体加载策略

  • 使用font-display: swap避免闪烁
  • 预加载关键字体
  • 实现字体子集化
  • 提供合适的回退字体

3. 性能优化

  • 减少字体文件数量
  • 使用字体子集
  • 实现懒加载
  • 监控字体加载性能

4. 用户体验

  • 避免字体闪烁
  • 提供加载状态指示
  • 实现渐进式字体加载
  • 处理字体加载失败

通过合理的字体优化策略,可以显著减少字体文件大小,提升页面加载速度,避免字体闪烁,改善用户体验。