阿里云主机折上折
  • 微信号
Current Site:Index > Font file optimization and subsetting

Font file optimization and subsetting

Author:Chuan Chen 阅读数:30291人阅读 分类: 性能优化

The Necessity of Font File Optimization and Subsetting

Font files are indispensable resources in web design, but unprocessed fonts often have large file sizes. A complete English font package may exceed 100KB, while Chinese fonts can surpass 5MB. This size significantly impacts webpage performance, especially for mobile users who may experience loading delays and excessive data consumption. Font subsetting dramatically reduces file size by extracting only the characters actually used. For example, a page using only 100 Chinese characters can see its font file shrink from 5MB to around 50KB after subsetting.

Choosing Font File Formats

Modern web pages primarily use four font formats: WOFF, WOFF2, TTF, and EOT. WOFF2 currently offers the highest compression rate, typically 30% smaller than WOFF. In practice, projects should prioritize WOFF2 and provide WOFF as a fallback:

@font-face {
  font-family: 'OptimizedFont';
  src: url('font.woff2') format('woff2'),
       url('font.woff') format('woff');
  font-weight: normal;
  font-style: normal;
}

For Chinese web pages, TTF should be the last fallback option due to its uncompressed nature, which results in large files. The EOT format is mainly for compatibility with older browsers like IE8 and can be phased out in modern projects.

Implementing Character Subsetting Techniques

Subsetting Based on Unicode Ranges

Tools like pyftsubset allow extracting specific characters using Unicode ranges:

pyftsubset SourceHanSansSC-Regular.ttf \
--output-file="Subset.ttf" \
--text="你好世界" \
--flavor=woff2

This method is suitable when the exact character set is known. For dynamic content websites, backend technologies can generate real-time subsets:

// Node.js example: Dynamically generating font subsets
const fontSubset = require('font-subset');
app.get('/dynamic-font', (req, res) => {
  const text = decodeURIComponent(req.query.text);
  fontSubset('font.ttf', text).then(subsetBuffer => {
    res.type('application/font-woff2');
    res.send(subsetBuffer);
  });
});

Automated Subset Generation Solutions

Modern build tools like Webpack can automate subset generation using plugins. For example, with webpack-subset-font-loader:

module: {
  rules: [
    {
      test: /\.(ttf|otf)$/,
      use: [
        {
          loader: 'webpack-subset-font-loader',
          options: {
            text: function() {
              // Extract actual text used from HTML/JS
              return fs.readFileSync('src/index.html', 'utf8') + 
                     fs.readFileSync('src/main.js', 'utf8');
            },
            formats: ['woff2', 'woff']
          }
        }
      ]
    }
  ]
}

Dynamic Subset Loading Strategies

For single-page applications with dynamic content, a phased loading strategy can be employed. Load a base subset for the initial screen and dynamically load additional characters based on routing or user interaction:

// Base subset includes 300 commonly used Chinese characters
const baseSubset = loadFont('font-base.woff2');

// Load additional characters for article pages
router.on('/article/:id', async () => {
  const articleText = await fetchArticle();
  const extraChars = extractUniqueChars(articleText);
  loadDynamicFontSubset(extraChars);
});

function loadDynamicFontSubset(chars) {
  const existing = getUsedChars();
  const newChars = chars.filter(c => !existing.includes(c));
  if (newChars.length) {
    const subsetUrl = `/font-subset?chars=${encodeURIComponent(newChars.join(''))}`;
    const fontFace = new FontFace('DynamicFont', `url(${subsetUrl})`);
    fontFace.load().then(() => document.fonts.add(fontFace));
  }
}

Variable Font Optimization Techniques

Variable fonts combine multiple weight and width variants into a single file, allowing dynamic adjustments via axis parameters. A single variable font can often replace 4-6 static font files:

@font-face {
  font-family: 'VariableFont';
  src: url('font.woff2') format('woff2');
  font-weight: 100 900;
  font-stretch: 75% 125%;
}

body {
  font-family: 'VariableFont';
  font-weight: 400; /* Normal weight */
}

h1 {
  font-weight: 700; /* Bold */
}

Subsetting is still necessary for variable fonts. The --variations parameter in fonttools preserves variable features:

pyftsubset VariableFont.ttf \
--output-file="VariableSubset.woff2" \
--text="ABCDEabcde12345" \
--flavor=woff2 \
--variations="wght=300:700 wdth=75:125"

Font Loading Performance Optimization

Controlling Font Display Strategies

The CSS font-display property controls text rendering behavior during font loading:

@font-face {
  font-family: 'OptimizedFont';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* Display fallback first, then swap to custom font */
}

Preloading Critical Fonts

For critical above-the-fold fonts, use <link rel="preload"> to load them early:

<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

Font Loading State Detection

The JavaScript FontFace API provides precise control over font loading:

document.fonts.load('1em OptimizedFont').then(() => {
  document.documentElement.classList.add('fonts-loaded');
});

// Corresponding CSS
body {
  font-family: fallback-font;
}
.fonts-loaded body {
  font-family: OptimizedFont, fallback-font;
}

Case Studies

A news website achieved a 72% improvement in font loading performance with the following optimizations:

  1. Base subset: 350 commonly used Chinese characters (45KB)
  2. Dynamic supplementation: Real-time loading of additional characters based on user content
  3. Caching strategy: Local storage of loaded characters to avoid duplicate requests
  4. Fallback: System fonts for initial rendering

Implementation snippet:

// Check locally cached font subsets
function getCachedFontSubset() {
  const cached = localStorage.getItem('fontCache');
  return cached ? JSON.parse(cached) : [];
}

// Update font cache
function updateFontCache(newChars) {
  const existing = getCachedFontSubset();
  const updated = [...new Set([...existing, ...newChars])];
  localStorage.setItem('fontCache', JSON.stringify(updated));
  return updated;
}

// Merge base subset with cached characters
async function loadOptimizedFont() {
  const baseSubset = await fetch('/fonts/base-subset.woff2');
  const cachedChars = getCachedFontSubset();
  if (cachedChars.length) {
    const dynamicSubset = await fetch(`/fonts/dynamic?chars=${cachedChars.join('')}`);
    // Create merged font face
  }
}

Advanced Optimization Techniques

Glyph Sharing and Reuse

For multilingual websites, analyzing shared glyphs across languages can further optimize:

# Using fontTools to analyze shared Chinese characters in CJK fonts
from fontTools.ttLib import TTFont
from fontTools.unicode import Unicode

jp_font = TTFont('Japanese.otf')
cn_font = TTFont('Chinese.otf')

jp_chars = set(c for t in jp_font['cmap'].tables for c in t.cmap.keys())
cn_chars = set(c for t in cn_font['cmap'].tables for c in t.cmap.keys())

common_chars = jp_chars & cn_chars
print(f"Shared Chinese characters: {len(common_chars)}")

Incremental Update Strategy

Implement client-side glyph incremental updates:

class FontDeltaLoader {
  constructor(baseVersion) {
    this.baseVersion = baseVersion;
    this.loadedGlyphs = new Set();
  }

  async checkUpdate() {
    const response = await fetch(`/font-version?current=${this.baseVersion}`);
    const { latestVersion, delta } = await response.json();
    
    if (latestVersion !== this.baseVersion) {
      await this.applyDelta(delta);
      this.baseVersion = latestVersion;
    }
  }

  async applyDelta(delta) {
    const newGlyphs = delta.filter(g => !this.loadedGlyphs.has(g));
    if (newGlyphs.length) {
      const fontFace = await this.loadGlyphs(newGlyphs);
      document.fonts.add(fontFace);
      newGlyphs.forEach(g => this.loadedGlyphs.add(g));
    }
  }
}

Browser Compatibility Implementation

Adopt tiered optimization strategies for different browsers:

  1. Modern browsers: WOFF2 variable fonts + dynamic subset loading
  2. Moderate browsers: Static WOFF subsets + font-display
  3. Legacy browsers: System fonts + critical CSS inlining

Build configuration example:

// Generate different resources based on browser support
const browserslist = require('browserslist');
const supported = browserslist('> 0.5%, last 2 versions');

module.exports = {
  plugins: [
    new FontSubsetPlugin({
      targets: {
        modern: supported.includes('chrome 90') ? 
          { formats: ['woff2'], variations: true } :
          { formats: ['woff'], static: true }
      }
    })
  ]
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.