Multilingual font stack: Cyrillic / CJK / Arabic
Fallback chain для проекта с несколькими письменностями: что грузить, что брать из системы, как тестировать отсутствие tofu (□).
Действуй как типограф интернационализации. Собери fallback chain для шрифта {{primary_font}} с поддержкой письменностей {{scripts}} так, чтобы на любом языке не было tofu (□), бандл не вырос вдвое, а первая отрисовка прошла быстро.
1. Архитектура: один файл vs unicode-range
Полный Inter с латиницей + кириллицей + греческим + vietnamese весит 380KB в woff2 на каждый вес. С CJK — 4-10MB на JP, 20MB+ на full CJK. Грузить всё сразу — самоубийство для LCP.
Решение: unicode-range в @font-face. Браузер скачивает только тот subset, который реально нужен текущему контенту страницы.
/* Latin core */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/inter-latin.woff2') format('woff2-variations');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* Cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/inter-cyrillic.woff2') format('woff2-variations');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Cyrillic Extended */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/inter-cyrillic-ext.woff2') format('woff2-variations');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
U+A640-A69F, U+FE2E-FE2F;
}
Google Fonts отдаёт ровно такую разбивку для всех своих шрифтов — открой ссылку на CSS, скопируй unicode-range оттуда. Не пиши руками.
2. CJK — отдельная стратегия
Japanese, Chinese (Simplified/Traditional), Korean — каждый шрифт 3-15MB. Грузить как Inter нельзя.
Варианты:
-
System fallback only (рекомендую для UI):
font-family: 'Inter', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic Medium', 'Meiryo', 'Microsoft YaHei', 'PingFang SC', 'Malgun Gothic', sans-serif;У всех современных ОС есть хорошие CJK-шрифты — пользователи уже привыкли.
-
Noto Sans CJK с subsetting под язык страницы:
/fonts/noto-jp.woff2подгружается только когдаlang="ja"- Используй
unicode-rangeдля базовых hiragana/katakana, остальное — system
-
Variable Noto Sans (новинка 2024-2025): один файл, веса 100-900, но всё равно 4-8MB на скрипт. Только для proper typography-heavy сайтов.
3. Arabic / Hebrew — RTL-нюансы
Шрифт должен иметь Arabic-glyphs И поддерживать RTL-shaping (лигатуры, контекстные формы).
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-arabic.woff2') format('woff2');
unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E,
U+0890-0891, U+0898-08E1, U+08E3-08FF,
U+200C-200E, U+2010-2011, U+204F, U+2E41,
U+FB50-FDFF, U+FE70-FEFC, U+102E0-102FB;
}
Inter покрывает Arabic с версии 4.0. Geist — нет, fallback на Noto Sans Arabic.
Hebrew: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F.
<html lang="ar" dir="rtl">
dir="rtl" обязательно — без него лигатуры ломаются, числа рендерятся в обратном порядке.
4. Каноничный fallback chain для интерфейса
:root {
--font-sans:
{{primary_font}},
/* Latin/Cyrillic system fallback */
-apple-system,
BlinkMacSystemFont,
'Segoe UI Variable',
'Segoe UI',
/* CJK fallback by OS */
'Hiragino Sans',
'Yu Gothic UI',
'Meiryo',
'PingFang SC',
'Microsoft YaHei',
'Malgun Gothic',
/* Generic */
Roboto,
Helvetica,
Arial,
sans-serif,
/* Emoji */
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Noto Color Emoji';
}
Порядок важен. Браузер идёт слева направо и проверяет на каждом шрифте: есть ли глиф для конкретного символа. Первый, у кого есть — побеждает на этом символе.
Emoji в конце — обязательно. Иначе на Windows эмодзи рендерятся в monochrome через Segoe UI Symbol.
5. Variable fonts + multilingual
Variable Inter / Variable Noto экономят запросы (один файл вместо 9 весов), но subset по unicode-range всё равно обязателен. Иначе грузишь 4MB вместо 200KB.
6. Subsetting своими руками (если нужен кастомный шрифт)
# pyftsubset из fonttools
pyftsubset MyFont.ttf \
--unicodes="U+0020-007F,U+00A0-00FF,U+0400-045F,U+0490-0491,U+2116" \
--layout-features="kern,liga,calt,locl,tnum,lnum,onum,ss01" \
--flavor=woff2 \
--output-file=myfont-latin-cyrillic.woff2
locl (localized forms) критично для кириллицы: болгарский «б» и сербский «бде» отличаются от русских.
7. Как тестировать отсутствие tofu
Tofu (□) — это маленький квадратик, который браузер показывает, если ни один шрифт в стеке не имеет глифа для символа.
-
Текстовый смок-тест. Скрипт, который рендерит на одной странице:
- Латиницу:
The quick brown fox jumps over the lazy dog - Расширенная латиница:
Ñoño, Façade, Mötörhead, Ærø, Žluťoučký - Кириллица:
Съешь же ещё этих мягких французских булок, да выпей чаю - Кириллица расширенная:
Ҕ ҥ ӕ ӂ ѣ ѫ ѧ Ѵ(старославянские) - Greek:
Φαγέδαινα ἥξει - Arabic:
نص حكيم له سر قاطع وذو شأن عظيم - CJK JP:
いろはにほへとちりぬるを我が宿の梅の花 - CJK CN:
视而不见,听而不闻 - CJK KR:
키스의 고유조건은 입술끼리 만나야 하고 - Numbers tabular:
0123456789 - Punctuation:
«» „" '' – — … § ¶ † ‡ № ™ - Math/currency:
€ ₽ ¥ ₿ ∞ ≈ ≠ ≤ ≥ ± × ÷ - Emoji:
🚀 ✨ 📝
Глазами проверь — нет ли квадратиков.
- Латиницу:
-
Автоматизированно через Playwright + visual diff:
await page.goto('/font-smoke-test'); await expect(page).toHaveScreenshot('fonts-baseline.png', { maxDiffPixels: 100, });Если шрифт деградировал на fallback — diff покажет.
-
DevTools → Computed → Rendered Fonts. Открой инспектор на конкретном слове, посмотри какой шрифт реально использован. Если для кириллицы используется Times New Roman — у тебя в стеке дыра.
-
Throttle 3G + Slow CPU. Прогрузи страницу с throttling — увидишь FOUT (flash of unstyled text), на котором проявляются дыры fallback'а.
8. font-display стратегия
swap— для основного шрифта на body. Видим текст сразу, шрифт долетает за 200-2000ms.optional— для премиум-шрифтов, где допустим fallback навсегда (если не успел за 100ms — забываем).fallback— компромисс: 100ms блокировки, потом swap, потом fallback навсегда.block— НИКОГДА на основной шрифт. Только для иконочных шрифтов где fallback невозможен (но лучше SVG).
9. Формат вывода
## Multilingual font stack
### Scripts coverage
| Script | Source | Size |
|---|---|---|
| Latin | {{primary_font}} subset | 25KB |
| Latin-ext | {{primary_font}} subset | 12KB |
| Cyrillic | {{primary_font}} subset | 18KB |
| Cyrillic-ext | {{primary_font}} subset | 6KB |
| Greek | system fallback | 0 |
| Arabic | Noto Sans Arabic subset | 35KB |
| CJK JP | system fallback (Hiragino/Yu Gothic) | 0 |
### Fallback chain
```css
font-family: {{primary_font}}, -apple-system, BlinkMacSystemFont,
'Segoe UI', 'Hiragino Sans', 'Yu Gothic UI',
'PingFang SC', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji';
font-display
- body: swap
- display headline: optional
Тесты
- Смок-страница со всеми скриптами — нет tofu
- Playwright screenshot diff на 3 ОС
- DevTools Computed → Rendered Fonts для кириллицы и CJK
- Throttle 3G: fallback корректен
## Anti-patterns
- ❌ Один `@font-face` без `unicode-range` — грузишь 380KB латиницы пользователю, читающему только по-русски.
- ❌ Noto CJK 4MB подключен через стандартный `<link>` — LCP уезжает на +3 секунды, и так на любой странице.
- ❌ `font-display: block` на основном шрифте — FOIT на 3 секунды, пользователь думает что сайт сломан.
- ❌ Стек без CJK fallback (`Inter, sans-serif`) — для японского пользователя весь интерфейс рендерится дефолтным санс-серифом ОС, выглядит как 1998.
- ❌ Emoji-шрифт не последним в стеке — на Windows эмодзи monochrome через Segoe UI Symbol, грустно.
- ❌ Кастомный шрифт через base64 в CSS — кеш бесполезен, CSS раздут на 2MB.
- ❌ Тест шрифтов только латиницей — деплой, через час user из Болгарии пишет «у вас вместо буквы квадратик».
- ❌ Игнорирование `locl` при subsetting — болгарский и сербский кириллический рендерятся как русский, локалы недовольны.
- ❌ `@import url(googlefonts...)` в CSS вместо `<link rel="preload">` — render-blocking, +500ms к LCP.
- ❌ Хранение woff (не woff2) — woff2 жмёт на 30% лучше, поддерживается везде с 2017.
- ❌ Hebrew/Arabic без `dir="rtl"` на `<html>` — лигатуры ломаются, числа в обратном порядке, пользователь уходит.
- ❌ Загрузка JP-шрифта на странице с английским контентом по `lang="en"` — стоит подгружать по `:lang(ja)` селектору или динамически.
Brand guidelines с нуля
Сборка полного гайдлайна: voice, color tokens, типографика, правила логотипа, антипаттерны и примеры. Готовый DESIGN.md.
Подбор пары шрифтов
Headline + body шрифт которые работают вместе. Принципы, примеры, проверка.
Вертикальный ритм типографики
Line-height, spacing, baseline grid — текст который дышит и читается.