A faster fork of subfont that subsets web fonts to only the characters used on your pages. Adds parallel tracing, disk caching, woff2-only output, always-on variable font instancing, and is fully written in TypeScript (the upstream is JavaScript). On turntrout.com (382 pages, 20+ font variants), switching to this fork cut font subsetting from 111 minutes to 28 minutes.
subfont produces dramatically smaller font files by stripping data that browsers never use:
| Optimization | Technique |
|---|---|
| Hinting removal | Strips TrueType hinting instructions (browsers auto-hint) |
| Name table pruning | Keeps only the 4 IDs browsers read (family, subfamily, full name, PostScript name) |
| Name lang-ID filter | Keeps only en-US name strings; drops Japanese, Russian, Korean, etc. |
| Table stripping | Drops DSIG, LTSH, VDMX, hdmx, gasp, PCLT |
| MATH-table drop (gated) | Drops MATH when no math codepoints are used on the page |
| Color-table drop (gated) | Drops COLR/CPAL/SVG /CBDT/CBLC/sbix/EBDT/EBLC/EBSC when no emoji used |
| Layout-script filter (gated) | Drops GSUB/GPOS lookups for OpenType scripts the page doesn't render |
| CSS-aware feature retention | Drops GSUB/GPOS features the page's CSS doesn't reference |
| Family-scoped page text (gated) | On shared-CSS pages, attributes a page's visible text only to the webfont families whose selectors can match an element on that page (falls back to all families when the font-family rules can't be parsed) |
| Non-rendered attribute skip | Excludes title/aria-label/aria-description text from subsets (tooltips render in the OS font; ARIA labels are never painted) |
Reproducible benchmark on testdata/subsetFonts/OpenSans-400.ttf (run with pnpm run build && node scripts/bench-readme.js); "upstream" = the subset-font package the original Munter/subfont uses, woff2-compressed:
| Text sample | Upstream subfont | @turntrout/subfont |
Savings |
|---|---|---|---|
| Heading (short) | 2,604 B | 828 B | 68% |
| Paragraph | 4,448 B | 2,072 B | 53% |
| Full page charset | 9,388 B | 5,500 B | 41% |
pnpm add -g @turntrout/subfont
Or with npm:
npm install -g @turntrout/subfont
Requires Node.js >= 18.
# Optimize build artifacts in-place (recommended)
subfont path/to/dist/index.html -i
# Preview without writing
subfont path/to/dist/index.html --dry-run
# Output to a separate directory
subfont path/to/index.html -o path/to/output
# Crawl all linked pages
subfont path/to/index.html -i --recursive
# Trace JS-rendered content in headless Chrome
subfont path/to/index.html -i --dynamic
# Cache subset results between runs
subfont path/to/index.html -i --cache| Flag | Default | Description |
|---|---|---|
-i, --in-place |
off | Modify files in-place |
-o, --output |
Output directory | |
--root |
Path to web root (deduced from input files if not specified) | |
--canonical-root |
URI root where the site will be deployed | |
-r, --recursive |
off | Crawl linked pages |
--dynamic |
off | Trace with headless browser |
--dry-run |
off | Preview without writing |
--fallbacks |
on | Async-load the full original font as a fallback for dynamic content |
--font-display |
swap |
auto/block/swap/fallback/optional |
--text |
Extra characters for every subset | |
--cache [dir] |
off | Cache subset results to disk between runs |
--concurrency N |
auto | Max worker threads (defaults to CPU count, max 8). Warns when exceeding memory-based estimate (~50 MB per worker) |
--chrome-flags |
Custom Chrome flag for --dynamic. Use the = form and repeat for multiple flags: --chrome-flags=--no-sandbox --chrome-flags=--disable-features=Foo,Bar |
|
--source-maps |
off | Preserve CSS source maps (slower) |
--strict |
off | Exit non-zero if any warnings are emitted |
-s, --silent |
off | Suppress all console output |
-d, --debug |
off | Verbose timing and font glyph detection info |
--relative-urls |
off | Emit relative URLs instead of root-relative |
--inline-css |
off | Inline the subset @font-face CSS into HTML |
Run subfont --help for the full list.
| Variable | Description |
|---|---|
PUPPETEER_EXECUTABLE_PATH |
Path to a Chrome/Chromium binary; skips auto-download when --dynamic is used |
To include extra characters in a specific font's subset, add -subfont-text to its @font-face:
@font-face {
font-family: Roboto;
src: url(roboto.woff2) format('woff2');
-subfont-text: '0123456789';
}const subfont = require('@turntrout/subfont');
const assetGraph = await subfont(
{
inputFiles: ['path/to/index.html'],
inPlace: true,
},
console
);The package ships CommonJS with TypeScript declarations (subfont.d.ts). Returns the Assetgraph instance.
Long runs can be cancelled with an AbortSignal (e.g. a timeout budget in CI):
const controller = new AbortController();
setTimeout(
() => controller.abort(new Error('subfont budget exceeded')),
600000
);
await subfont(
{
inputFiles: ['path/to/index.html'],
inPlace: true,
signal: controller.signal,
},
console
);subfont(options, console) — the second argument is an optional logger (anything
with log, warn, and error methods — e.g. the global console). Pass
null together with silent: true to suppress all output.
The options object accepts the following keys:
| Option | Type | Default | Description |
|---|---|---|---|
inputFiles |
string[] |
[] |
HTML entry points (file paths or URLs). At least one is required unless root is given. |
root |
string |
deduced | Path or URL to the web root. Deduced from inputFiles if omitted. |
canonicalRoot |
string |
— | URI root where the site will be deployed (used to rewrite absolute URLs). |
output |
string |
— | Output directory. Mutually exclusive with inPlace. |
inPlace |
boolean |
false |
Modify input files in place. |
dryRun |
boolean |
false |
Trace and compute subsets but do not write any files. |
recursive |
boolean |
false |
Crawl linked pages starting from inputFiles. |
dynamic |
boolean |
false |
Trace JS-rendered content in headless Chrome (via puppeteer). |
fallbacks |
boolean |
true |
Async-load the full original font as a fallback for dynamic content. |
fontDisplay |
string |
'swap' |
font-display CSS value: auto, block, swap, fallback, or optional. |
text |
string |
— | Extra characters to include in every subset. |
inlineCss |
boolean |
false |
Inline the subset @font-face CSS into the HTML document. |
relativeUrls |
boolean |
false |
Emit relative URLs instead of root-relative URLs. |
sourceMaps |
boolean |
false |
Preserve CSS source maps (slower). |
concurrency |
number |
auto | Max parallel tracing workers. Defaults to the CPU count, capped at 8. Exceeding the memory-based estimate (~50 MB per worker) warns. |
chromeFlags |
string[] |
[] |
Extra Chrome flags forwarded to puppeteer when dynamic is set. |
cache |
boolean | string |
false |
Cache subset results between runs. Pass a path to customize the cache directory; true uses .subfont-cache inside the root directory. |
strict |
boolean |
false |
Resolve with a non-zero exit (via the CLI) if any warnings are emitted. |
silent |
boolean |
false |
Suppress all log output to console. |
debug |
boolean |
false |
Emit verbose timing and glyph-detection info. |
signal |
AbortSignal |
— | Abort the run early. Cancels in-flight font tracing and subsetting; the returned promise rejects with the signal's reason. |
MIT -- Original work by Peter Muller (Munter)