open atlas
↑ К треку
Next.js с нуля до senior NEXT · 05 · 02

next/font и стратегии CSS: self-hosted шрифты, подмена без сдвига, стили без рантайма

next/font скачивает шрифты Google на билде и раздаёт сам — без рантайм-запроса к Google — с fallback-ом с подогнанными метриками: подмена ничего не сдвигает. Сабсеты и вариативные шрифты задают бюджет байтов; Tailwind и CSS Modules без рантайма, CSS-in-JS тянет клиентский бандл.

NEXT Middle ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

B2B-сайт получает за неделю две несвязанные жалобы. Юристы пересылают письмо: посетитель ссылается на мюнхенское судебное решение 2022 года — подключение Google Fonts с CDN Google передаёт Google IP-адреса посетителей без согласия; GDPR-проблема, о которой команда не знала, зашита в один тег <link href="https://fonts.googleapis.com/...">. Тем временем продакт показывает полевые данные: CLS 0,24 — далеко за порогом 0,1. Filmstrip объясняет почему: заголовки рендерятся Arial-ом, через 800 мс приезжает брендовый шрифт, и каждая строка текста переносится заново, потому что у двух шрифтов разные ширины и межстрочные интервалы; на медленных соединениях таблица тарифов ощутимо прыгает ровно в момент, когда пользователь тянется кликнуть. У обоих багов один фикс в один коммит: next/font скачивает шрифт на билде, отдаёт его с собственного origin сайта (из браузера посетителя к Google не уходит ни одного запроса) и генерирует fallback с подогнанными метриками — момент подмены не сдвигает ничего.

Что загрузчик шрифтов делает на билде

next/font/google — не рантайм-интеграция с Google Fonts, а загрузчик времени сборки. Когда вы вызываете загрузчик шрифта на уровне модуля, билд один раз забирает файлы у Google, сабсетит их и кладёт .woff2 (компактный формат шрифтов с хорошим сжатием, поддерживаемый всеми современными браузерами) в /_next/static/media с immutable-заголовками кеша, а сгенерированный @font-face инлайнится в ваш CSS. В продакшене браузер посетителя общается только с вашим origin: ни DNS-резолва fonts.googleapis.com, ни установления стороннего соединения (одно оно стоит несколько сотен миллисекунд на мобильных), ни IP-адреса, отправленного Google, — что и закрывает GDPR-риск из Hook. next/font/local так же упаковывает ваши собственные шрифты.

// app/layout.tsx — вызов загрузчика ОБЯЗАН быть константой на уровне модуля, по одной на шрифт
import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],     // обязательно для Google Fonts: отправляем только нужные глифы
  display: "swap",        // сразу показываем fallback, подменяем по готовности
  variable: "--font-sans" // экспонируем как CSS-переменную для Tailwind/CSS
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

Правило «только на уровне модуля» — несущее: каждый вызов загрузчика — один экземпляр шрифта, поднятый и дедуплицированный на билде. Вызовите его в теле компонента или во многих файлах с разными опциями — и начеканите несколько экземпляров: дублированные блоки @font-face и дублированные preload-ы. Масштабируемая конвенция: объявить все шрифты в одном файле, экспортировать экземпляры и подключить их один раз в корневом layout через className или variable.

Компромисс: загрузка на билде означает, что сборка теперь зависит от доступности серверов Google (CI-машина без egress валит билд), а обновления шрифта приезжают только с пересборкой. Взамен — герметичный продакшен: версия шрифта зафиксирована, файлы отдаются с вашего CDN-edge рядом с CSS и не зависят от сторонних аварий.

Убить шрифтовой CLS: size-adjust и метрики fallback

font-display: swap сразу показывает fallback-шрифт, а веб-шрифт подменяет его по прибытии. Для производительности это правильно — текст читается с первой отрисовки, — но именно подмена и порождает CLS: Arial и ваш брендовый шрифт расходятся в ширинах глифов, x-height, ascent и descent, и в момент подмены каждый абзац переносится заново, страница перетекает. Страницы с большим объёмом текста стабильно собирают 0,1–0,25 CLS на одном этом механизме, сжигая бюджет 0,1 на одной типографике.

Ответ next/font — adjustFontFallback (включён по умолчанию): на билде он вычисляет метрические оверрайды — size-adjust, ascent-override, descent-override, line-gap-override, — которые перекраивают локальный fallback (Arial для sans, Times для serif) так, чтобы он занимал ровно то же место, что и веб-шрифт, и эмитит этот подогнанный fallback вторым @font-face. Браузер сначала рендерит подогнанный fallback; когда приезжает настоящий шрифт, глифы меняют форму, но ни одна строка не переносится иначе и ничего не двигается. Подмена становится геометрически невидимой: пользователь видит смену шрифта, движок раскладки не видит ничего. Это разница между «мы используем font-display: swap» и «наша подмена шрифта даёт 0,00 в CLS» — первое стратегия загрузки, второе требует метрической математики, которую next/font делает за вас.

Викторина

Команда переносит шрифт с link-тега fonts.googleapis.com на next/font/google. Что теперь запрашивает браузер посетителя в продакшене и откуда?

Сабсеты, начертания и вариативные шрифты: бюджет байтов

Когда тянешься за веб-шрифтом, легко взять весь семейный файл, не задумываясь, сколько из него реально нужно сайту. Сколько байтов можно срезать — и срезать бесплатно — разберём по двум осям.

Полный набор символов семейства огромен — латиница плюс кириллица плюс греческий плюс вьетнамский, — а большинству сайтов нужна его доля. Опция subsets отгружает только объявленные диапазоны: одно статическое начертание рабочего гротеска в латинском сабсете — примерно 15–30 КБ woff2, файл без сабсета — в разы больше. Для двуязычного сайта объявляйте оба сабсета явно (subsets: ["latin", "cyrillic"]) — браузер скачивает пофайлово через unicode-range, только когда страница реально использует эти глифы.

Начертания — вторая ось. Каждое статическое начертание — отдельный файл, так что weight: ["400", "500", "600", "700"] — четыре загрузки. Вариативный шрифт упаковывает всю ось веса в один файл — обычно 40–100 КБ для латинского сабсета, — что выигрывает у статики, как только начертаний больше двух-трёх, и бесплатно открывает промежуточные значения. Честная бухгалтерия режет и в обратную сторону: если дизайн действительно использует только regular и bold, два статических файла по ~20 КБ обгоняют вариативный. next/font по умолчанию берёт вариативные оси Google-шрифтов, когда они доступны; решение принимайте по network-панели, а не по ощущениям.

Стратегии CSS и что они делают с бандлами

Шрифты — половина типографики; у CSS, который их применяет, своё поведение в бандле. Tailwind — zero-runtime по построению: сканирование исходников на билде генерирует один статический стилевой файл ровно из использованных утилит — обычно 10–50 КБ суммарно даже на больших приложениях, отдаётся один раз, кешируется навсегда, JavaScript не отгружается. CSS Modules — тоже zero-runtime: имена классов хешируются на билде для изоляции, а CSS код-сплитится вместе с чанками маршрутов, которые его импортируют, — страница тянет только стили своего дерева. Оба чисто стыкуются с next/font через паттерн CSS-переменной из сниппета — fontFamily в конфиге Tailwind просто ссылается на var(--font-sans).

С платформой воюет рантаймовый CSS-in-JS. Библиотеки вроде styled-components вычисляют стили в браузере: каждый styled-компонент обязан быть клиентским, движок стилей (~12+ КБ gzip ещё до первого вашего стиля) едет в бандл, а Server Components — которые в браузере не выполняются вовсе — без registry-обходного пути для стриминга им недоступны. Боевой провал: команда мигрирует на App Router, сохраняет дизайн-систему на styled-components и обнаруживает, что каждому листу UI-кита теперь нужен 'use client' — выбор стилизации молча продиктовал серверно-клиентскую архитектуру. Zero-runtime CSS в App Router — не вкусовщина, а то, что оставляет дереву компонентов свободу жить на сервере.

Викторина

С font-display: swap брендовый шрифт подменяет fallback через 800 мс после первой отрисовки, и полевой CLS относит 0,18 на сдвиг текста в этот момент. Каков фактический механизм, которым next/font это чинит?

Вспомните перед уходом
  1. 01
    Что происходит на билде, а что в рантайме с next/font/google, и почему важно правило module scope?
  2. 02
    Как next/font сводит вклад font-display: swap в CLS к нулю, и когда вариативный шрифт выгоднее статических начертаний?
Итог

next/font превращает шрифты из сторонней рантайм-зависимости в артефакты сборки. Вызов загрузчика — на уровне модуля, один на шрифт, общий для всех — скачивает файлы у Google во время next build, сабсетит до объявленных unicode-диапазонов и кладёт woff2 в /_next/static с immutable-кешированием; браузер посетителя не обращается к доменам Google вовсе, что разом удаляет установление стороннего соединения с критического пути и GDPR-риск отправки IP посетителей Google. Фикс сдвига раскладки — часть, которую команды недооценивают: font-display swap сразу рисует текст fallback-ом, а подмена перетекает страницу, потому что метрики у шрифтов разные, — adjustFontFallback на билде вычисляет size-adjust, ascent-, descent- и line-gap-оверрайды и эмитит fallback, занимающий в точности геометрию веб-шрифта: подмена меняет форму глифов и не двигает ничего, удерживая вклад типографики в CLS на нуле против бюджета 0,1. Байтами правят две оси: сабсеты (объявляйте latin и cyrillic явно, и пусть unicode-range дозирует загрузки; статическое начертание сабсета — ~15–30 КБ) и начертания, где один вариативный файл на 40–100 КБ обгоняет пофайловую статику после двух-трёх весов. Со стороны CSS Tailwind и CSS Modules — zero-runtime: один кешируемый статический файл стилей либо изолированный CSS, нарезанный по чанкам маршрутов, — и оба стыкуются с next/font через CSS-переменную; рантаймовый же CSS-in-JS везёт движок в клиентском бандле и заставляет каждый styled-компонент быть клиентским, молча диктуя серверно-клиентскую архитектуру со стороны стилизации. Теперь, когда аудит GDPR находит запросы к сторонним доменам шрифтов — один коммит с next/font закрывает вопрос; когда CLS-отчёт обвиняет типографику — первым делом проверяешь, включён ли adjustFontFallback.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.