npm i -D unicode-range-split
A CJK face is megabytes because it covers everything. Cut it down to the characters your site uses and the day someone writes a rare kanji, it falls out of your typeface. This splits the font in two instead, and drops nothing.
この見出しは、分割した書体で描かれています。
This page is set in the two files above, produced by this package from Noto Sans JP. Nothing here is a mock-up: the figures come from the build.
The text below is rendered in the split font. Green characters are in the file your browser already has. Type one that is not, and the browser fetches the other file — the readout is its own timing.
common tier 125 KiB — fetched when this page loaded.
rare tier 2.0 MiB — not fetched. Every character above is in the file you already have.
It reads the files you name, keeps every character they use in the common tier, and puts everything else behind a unicode-range. Run it whenever your writing changes; the site is not broken if you forget, it only fetches the second file more often.
// unicode-range-split.config.js
export default {
fonts: [
{
source: "src/fonts/NotoSansJP-400.ttf",
family: "Noto Sans JP",
outDir: "public/fonts",
cssPath: "src/app/noto.css",
// every file whose characters must be in the small tier
scan: ["src", "content", "messages"],
},
],
};npx unicode-range-split
import { splitFont } from "unicode-range-split";
const { css, tiers } = await splitFont({
source: "src/fonts/NotoSansJP-400.ttf",
family: "Noto Sans JP",
outDir: "public/fonts",
scan: ["src", "content"],
});
tiers.common.url; // "/fonts/NotoSansJP-400-common.1f4a9c2b.woff2"
tiers.rest.url; // fetched only when a page needs one of its glyphsThe common tier is declared first with no range, so it claims every character. The rare tier follows with an explicit range, and the later rule wins for the characters it names. Swap them and the second file is never fetched.
@font-face {
font-family: "Noto Sans JP";
src: url("/fonts/noto-common.woff2") format("woff2");
}
@font-face {
font-family: "Noto Sans JP";
src: url("/fonts/noto-rest.woff2") format("woff2");
unicode-range: U+4E18-4E19,U+4E32,U+4E39, /* …and the rest */;
}