Chinese, Japanese, and Korean font files can contain thousands of glyphs, while a landing page may use only a headline and a few labels. Font subsetting removes unused glyphs so the browser downloads only the characters the product actually needs.
A useful online subsetter is more than a file input around a font library. It needs Unicode-safe character collection, format validation, WOFF decompression and compression, missing-glyph reporting, preview isolation, object-URL cleanup, and clear limits. The following architecture is based on a production browser-local implementation using fonteditor-core and fflate.
What font subsetting changes
A font is a collection of glyph outlines plus mapping, metrics, naming, hinting, kerning, and other tables. Subsetting keeps the glyphs reachable from a requested set of Unicode code points and rewrites the related tables. This differs from generic compression: ZIP or Brotli can shrink bytes in transit, but they do not remove thousands of unused glyphs from the decoded font.
The biggest wins usually come from large CJK fonts, icon fonts, campaign fonts, and one-off branded headings. A Latin font that is already small may gain less, especially when kerning and hinting tables are retained.
| Input | Subset source | Typical output |
|---|---|---|
| Marketing headline font | Final headline and button text | Small WOFF for one page |
| CJK UI font | Product copy and supported punctuation | Language-specific WOFF files |
| Icon font | Icons used by the current bundle | Reduced icon font |
| Document font | Characters detected in one document | Portable TTF subset |
Use a browser-local processing pipeline
The page should own file selection, options, progress, preview, and errors. A utility module should accept a File and explicit options, then return a discriminated outcome. Keeping font parsing out of the React component makes failure cases testable and prevents UI state from leaking into the conversion layer.
For ordinary subsetting there is no reason to upload the font or the requested text. File.arrayBuffer reads the selected file into the current browser tab, fonteditor-core parses and rewrites it, and the result becomes a Blob for preview and download.
- Validate byte size and format before reading the entire file.
- Accept both trustworthy MIME values and file extensions because browsers often leave font MIME types empty.
- Return typed error codes such as unsupported_input, empty_chars, parse_failed, and no_glyphs.
- Keep a maximum input size so one accidental font collection cannot exhaust the tab.
Collect Unicode code points, not UTF-16 units
JavaScript string indexing works in UTF-16 code units. Some characters outside the Basic Multilingual Plane use a surrogate pair, so splitting with text.split('') can turn one character into two invalid values. Iterate with for...of or Array.from, read codePointAt(0), and deduplicate the numeric code points with a Set.
Ignore C0 and C1 control characters, but do not discard whitespace blindly: a normal space is a real glyph and should often be kept. Also report characters that the source font does not contain instead of silently suggesting complete coverage.
Create and write the subset with fonteditor-core
fonteditor-core can parse TTF and WOFF inputs, select glyphs by Unicode code point, and write TTF or WOFF output. WOFF uses compressed table data, so fflate adapters provide the inflate and deflate callbacks. The same options should be applied consistently at parse and write time.
import { createFont } from 'fonteditor-core';
import { deflateSync, inflateSync } from 'fflate';
const codePoints = [...new Set(
Array.from(text, char => char.codePointAt(0)!)
)];
const input = await file.arrayBuffer();
const font = createFont(input, {
type: 'ttf',
subset: codePoints,
hinting: false,
kerning: false,
inflate: data => Array.from(inflateSync(new Uint8Array(data))),
});
const output = font.write({
type: 'woff',
deflate: data => Array.from(deflateSync(new Uint8Array(data))),
});- Disable hinting for smaller web assets unless the target rendering environment needs it.
- Keep kerning when typographic quality matters, but measure the size difference for the actual font.
- Name output files predictably, for example brand.subset.woff.
- Treat WOFF2 as a separate capability; fonteditor-core in this pipeline writes WOFF, not WOFF2.
Verify coverage and show useful metrics
A successful write does not prove that every requested character exists. Inspect the Unicode mappings on the resulting glyphs, compare them with the requested code points, and return included and missing character lists. This turns a vague font preview into an actionable validation result.
Show the original size, output size, saved bytes, saved percentage, requested count, included count, and glyph count. A negative saving is possible for an already tiny font or an output format with different overhead, so the UI should display the actual value rather than promising every subset is smaller.
Preview with a temporary font and clean it up
The output can be previewed without sending it anywhere. Wrap the ArrayBuffer in a Blob, create an object URL, and load it through FontFace or a generated @font-face rule with a unique family name. Render only known-included characters so browser fallback fonts do not hide missing glyphs.
const blob = new Blob([output], { type: 'font/woff' });
const url = URL.createObjectURL(blob);
const face = new FontFace('SubsetPreview', `url(${url})`);
await face.load();
document.fonts.add(face);
// Revoke the previous URL when regenerating or leaving the page.
URL.revokeObjectURL(url);- Revoke the previous object URL before replacing the result.
- Remove the loaded FontFace when the preview is no longer needed.
- Use a unique family name per result to avoid cached preview confusion.
- Download from the Blob URL and remove the temporary anchor immediately afterward.
Production limits and font edge cases
Variable fonts, color emoji fonts, OpenType layout rules, ligatures, and complex scripts need more care than a simple Unicode list. A text sample may require shaping-related glyphs that are not directly mapped from individual characters. Test the actual languages, browsers, and font licenses before shipping generated assets.
For a static website, build-time subsetting with a mature font tool may be more reproducible. The online approach is valuable for exploration, one-off assets, internal workflows, and privacy-sensitive files; it should not replace typography QA.
Key takeaways
A dependable online font subsetter combines Unicode-correct input handling, typed validation, real glyph coverage checks, explicit table options, and careful Blob lifecycle management. Keep the parser in a utility layer, make missing characters visible, and test complex fonts rather than assuming every OpenType file behaves like a basic Latin TTF.