unicode-range and font slicing
There is a useful unicode-range feature in the @font-face css at-rule. The unicode-range
can be used to:
- Specify the character (glyph) set to which a font can be applied.
- Slice the font files into different parts, especially grouping frequently used characters into a set, to reduce the total size to download
In this post, I will focus on the popular Japanese font Noto Sans JP, and consider the two use cases:
- The website is dynamic, any Japanese character can be used.
- The website is static, and the set of Japanese characters is fixed.
Precheck
First, get the list of google fonts via Google font API
curl https://www.googleapis.com/webfonts/v1/webfonts?key=<API_KEY>
The result is pasted in this gist. Search for "Noto Sans JP" and get the portion:
{
"family": "Noto Sans JP",
"variants": [
"100",
"200",
"300",
"regular",
"500",
"600",
"700",
"800",
"900"
],
"subsets": [
"cyrillic",
"japanese",
"latin",
"latin-ext",
"vietnamese"
],
"version": "v52",
"lastModified": "2023-05-02",
"files": {
"100": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEi75vY0rw-oME.ttf",
"200": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFJEj75vY0rw-oME.ttf",
"300": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFE8j75vY0rw-oME.ttf",
"regular": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEj75vY0rw-oME.ttf",
"500": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFCMj75vY0rw-oME.ttf",
"600": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFM8k75vY0rw-oME.ttf",
"700": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFPYk75vY0rw-oME.ttf",
"800": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFJEk75vY0rw-oME.ttf",
"900": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFLgk75vY0rw-oME.ttf"
},
"category": "sans-serif",
"kind": "webfonts#webfont",
"menu": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEj35rS1g.ttf"
}
There are 9 different files for various weights, each file is around 5.5MB. If you declare each @font-face with these files, the website will load about 50MB. That is a huge waste.
With WOFF2 format
curl https://www.googleapis.com/webfonts/v1/webfonts?capability=WOFF2&key=<API_KEY>
{
"family": "Noto Sans JP",
"variants": [
"100",
"200",
"300",
"regular",
"500",
"600",
"700",
"800",
"900"
],
"subsets": [
"cyrillic",
"japanese",
"latin",
"latin-ext",
"vietnamese"
],
"version": "v52",
"lastModified": "2023-05-02",
"files": {
"100": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEi757Y0rw-oME.woff2",
"200": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFJEj757Y0rw-oME.woff2",
"300": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFE8j757Y0rw-oME.woff2",
"regular": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEj757Y0rw-oME.woff2",
"500": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFCMj757Y0rw-oME.woff2",
"600": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFM8k757Y0rw-oME.woff2",
"700": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFPYk757Y0rw-oME.woff2",
"800": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFJEk757Y0rw-oME.woff2",
"900": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFLgk757Y0rw-oME.woff2"
},
"category": "sans-serif",
"kind": "webfonts#webfont",
"menu": "https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEj35rS0w.woff2"
}
For WOFF2 format, each weight takes ~2.2MB, leading to a total of ~20MB.
Font slicing
Suppose that you need to use all characters (glyphs) from this font, for example when your website is dynamic, you don't control what characters will be displayed statically.
In 2018, Google devs released a smart strategy to load Japanese font: see their blog (snapshot). Basically, the idea is
1. Take the 2,000 most popular Korean characters, sort by frequency, and put them into 20 equally sized slices
2. Sort the remaining characters by Unicode codepoint number, and divide them into 100 equally sized slices
For Japanese, we found that segmenting the first 3,000 characters into 20 slices was best, resulting in clients downloading 80% fewer bytes than they would if we just sent the whole font. Having sufficiently reduced transfer sizes, we now feel confident in offering Japanese web fonts for the first time!
They also found that
The core features we rely on to efficiently deliver sliced fonts are unicode-range and woff2
Browsers that support unicode-range and woff2 also support HTTP/2
HTTP/2 enables the concurrent delivery of many small files
So that browsers that have this feature enabled do not get much overhead degradation due to file slicing.
Analyse the Google font-slicing result
The css rule for Noto Sans JP weight 400 can be obtained via Google CSS API: https://fonts.googleapis.com/css?family=Noto+Sans+JP (result snapshot)
For full weight from 100 to 900: https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900 (result snapshot)
There are 124 slices in total:
- 120 numbered slices
- cyrillic
- vietnamese
- latin-ext
- latin
The result matches with the strategy described in the google dev blog.
Make use of unicode-range made by google font
The unicode-range defined by css rule gives us some useful usage:
- Now you have access to the Unicode set of all Japanese characters.
- You have access to the set of the most frequently used 3000 Japanese characters.
- If you want to slice your custom font to optimize the loading, you can re-use these values. All you need is a tool to extract a set of glyphs from a font.
- If you want to use this font for Japanese characters only, and use another font, .e.g. Inter, for other characters. You can just drop the last 4 @font-face definitions.
List all characters for each slice
The list of characters for each slice can be checked in this demo.
There is another site that lists all kanji.
Serve file locally
How to extract a specific set of glyphs from a font file
To look for Japanese characters from a text
Unicode character class escape \p{}
\P{}
is a convenient way to extract Japanese characters (hiragana, katakana, kanji, etc.)
- For Hiragana:
\p[Hiragana]
or[\u3041-\u3096]
Note that: [あ-ん]
does not cover all characters, such as ゔ
is not included.
- For Full width Katakana:
\p{Katakana}
or[\u30A0-\u30FF]
- Kanji:
\p{Han}
or[\u3400-\u4DB5\u4E00-\u9FCB\uF900-\uFA6A]
- Half-width Katakana and Punctuation:
[\u2E80-\u2FD5]
⦅ ⦆ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ テ ト ナ ニ ヌ ネ ノ ハ ヒ フ ヘ ホ マ ミ ム メ モ ヤ ユ ヨ ラ リ ル レ ロ ワ ン ゙
- FullWidth alphanumeric and punctuation:
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
@ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _
` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
- Kanji Radicals:
[\u2E80-\u2FD5]
- Japanese symbols and punctuation
[\u3000-\u303F]
、 。 〃 〄 々 〆 〇 〈 〉 《 》 「 」 『 』 【 】 〒 〓 〔 〕 〖 〗 〘 〙 〚 〛 〜 〝 〞 〟 〠 〡 〢 〣 〤 〥 〦 〧 〨 〩 〪 〫 〬 〭 〮 〯 〰 〱 〲 〳 〴 〵 〶 〷 〸 〹 〺 〻 〼 〽 〾 〿
- Miscellaneous Japanese Symbols and Characters:
[\u31F0-\u31FF\u3220-\u3243\u3280-\u337F]
ㇰ ㇱ ㇲ ㇳ ㇴ ㇵ ㇶ ㇷ ㇸ ㇹ ㇺ ㇻ ㇼ ㇽ ㇾ ㇿ
㈠ ㈡ ㈢ ㈣ ㈤ ㈥ ㈦ ㈧ ㈨ ㈩ ㈪ ㈫ ㈬ ㈭ ㈮ ㈯ ㈰ ㈱ ㈲ ㈳ ㈴ ㈵ ㈶ ㈷ ㈸ ㈹ ㈺ ㈻ ㈼ ㈽ ㈾ ㈿ ㉀ ㉁ ㉂ ㉃
㊀ ㊁ ㊂ ㊃ ㊄ ㊅ ㊆ ㊇ ㊈ ㊉ ㊊ ㊋ ㊌ ㊍ ㊎ ㊏ ㊐ ㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ ㊝ ㊞ ㊟ ㊠ ㊡ ㊢ ㊣ ㊤ ㊥ ㊦ ㊧ ㊨ ㊩ ㊪ ㊫ ㊬ ㊭ ㊮ ㊯ ㊰ ㊱ ㊲ ㊳ ㊴ ㊵ ㊶ ㊷ ㊸ ㊹ ㊺ ㊻ ㊼ ㊽ ㊾ ㊿
㋀ ㋁ ㋂ ㋃ ㋄ ㋅ ㋆ ㋇ ㋈ ㋉ ㋊ ㋋ ㋐ ㋑ ㋒ ㋓ ㋔ ㋕ ㋖ ㋗ ㋘ ㋙ ㋚ ㋛ ㋜ ㋝ ㋞ ㋟ ㋠ ㋡ ㋢ ㋣ ㋤ ㋥ ㋦ ㋧ ㋨ ㋩ ㋪ ㋫ ㋬ ㋭ ㋮ ㋯ ㋰ ㋱ ㋲ ㋳ ㋴ ㋵ ㋶ ㋷ ㋸ ㋹ ㋺ ㋻ ㋼ ㋽ ㋾
㌀ ㌁ ㌂ ㌃ ㌄ ㌅ ㌆ ㌇ ㌈ ㌉ ㌊ ㌋ ㌌ ㌍ ㌎ ㌏ ㌐ ㌑ ㌒ ㌓ ㌔ ㌕ ㌖ ㌗ ㌘ ㌙ ㌚ ㌛ ㌜ ㌝ ㌞ ㌟ ㌠ ㌡ ㌢ ㌣ ㌤ ㌥ ㌦ ㌧ ㌨ ㌩ ㌪ ㌫ ㌬ ㌭ ㌮ ㌯ ㌰ ㌱ ㌲ ㌳ ㌴ ㌵ ㌶ ㌷ ㌸ ㌹ ㌺ ㌻ ㌼ ㌽ ㌾ ㌿ ㍀ ㍁ ㍂ ㍃ ㍄ ㍅ ㍆ ㍇ ㍈ ㍉ ㍊ ㍋ ㍌ ㍍ ㍎ ㍏ ㍐ ㍑ ㍒ ㍓ ㍔ ㍕ ㍖ ㍗ ㍘ ㍙ ㍚ ㍛ ㍜ ㍝ ㍞ ㍟ ㍠ ㍡ ㍢ ㍣ ㍤ ㍥ ㍦ ㍧ ㍨ ㍩ ㍪ ㍫ ㍬ ㍭ ㍮ ㍯ ㍰ ㍱ ㍲ ㍳ ㍴ ㍵ ㍶ ㍻ ㍼ ㍽ ㍾ ㍿
Source for these sets: here or here or here or here.
The list of Unicode properties (blocks/) can be found here.
To download font files for local serving or for other processing
The google-webfonts-helper can be useful.
To extract only required glyphs from a font
If your website is static, and you know exactly what will be rendered, you can extract only required glyphs from a font file and make a new file with much smaller file size.
fonttools is a handy tool for doing this.
To install
python -m venv venv
. venv/bin/activate
pip install fonttools 'fonttools[ufo,lxml,woff,unicode]' brotli
To create subset font
. venv/bin/activate
pyftsubset input.woff2 --flavor=woff2 --unicodes-file=unicode.txt --output-file=output.woff2
Where unicode.txt
contains allowed characters in unicode format
This project provides the code to create subset for Korean font, which can be generalized to other languages.