Оптимізація шрифтів (next/font) у Next.js
next/font - це вбудована система роботи зі шрифтами у Next.js, яка завантажує шрифти під час збірки, прибирає зовнішні мережеві запити та усуває зсув макета (CLS) через автоматичний size-adjust.
Теорія
TL;DR
next/fontзавантажує шрифти під часnext buildі роздає їх з твого домену, без звернень до серверів Google на рантаймі- Класичний
<link href="fonts.googleapis.com">блокує рендер і викликає CLS;next/fontвирішує обидві проблеми classNameодразу застосовує стекfont-familyдо елемента;variableстворює CSS-змінну типу--font-interдля використання в Tailwind або globals.cssdisplay: 'swap'показує fallback-шрифт миттєво, потім замінює його на кастомний після завантаження- Для будь-якого шрифту в продакшн Next.js-проєкті використовуй
next/font; пропускай тільки для статичних сайтів без JS, де CLS не критичний
Швидкий приклад
// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body className={inter.className}>
{children}
</body>
</html>
);
}
// Під час збірки: WOFF2 завантажено, @font-face вбудовано в CSS,
// <link rel="preload" as="font"> додано до <head> автоматичноhtml отримує CSS-змінну, body отримує клас з font-family. Обидва потрібні, якщо хочеш і Tailwind-інтеграцію, і пряме застосування шрифту.
Класичні Google Fonts проти next/font
Старий спосіб додає <link href="https://fonts.googleapis.com/css2?family=Inter"> у <head>. Цей тег запускає DNS-запит, завантажує CSS-файл, потім завантажує самі файли шрифтів. Поки тривають ці мережеві запити, браузер рендерить текст у fallback-шрифті іншого розміру. Коли кастомний шрифт нарешті прибуває, текст стрибає. Цей стрибок і є CLS.
next/font прибирає всі ці запити. Шрифти потрапляють у білд-output як статичні WOFF2-файли. Правило @font-face генерується в CSS під час збірки. А size-adjust обчислюється з реальних метрик шрифту (ascent, descent, line-gap) так, щоб fallback максимально відповідав розмірам кастомного шрифту і зсуву не було видно.
Коли використовувати
- Потрібен новий Google Font: імпортуй з
next/font/google, все, більше нічого налаштовувати - Кастомний або платний шрифт у WOFF2: використовуй
next/font/localіз шляхом відносно файлу де визначаєш шрифт - Кілька шрифтів (заголовок + текст + моноширинний): визначи кожен окремо, застосуй CSS-змінні, підключи через конфіг Tailwind
- Variable font з діапазоном ваг: додай
axes: ['wght'], щоб один WOFF2-файл покривав усі ваги - Вже використовуєш
<link>-теги: мігруй наnext/fontдо релізу в продакшн, CLS безпосередньо впливає на Core Web Vitals
Як це працює під час збірки
Під час next build Next.js читає конфіги шрифтів, завантажує файли з Google (або бере твої локальні), конвертує у WOFF2 якщо потрібно, і записує у білд-output. Генерує правило @font-face з size-adjust, обчисленим із вертикальних метрик шрифту. Це правило потрапляє в глобальний CSS. Тег <link rel="preload" as="font"> автоматично додається у <head>, щоб браузер почав завантажувати шрифт якомога раніше у waterfall.
На рантаймі нема жодних зовнішніх запитів. Шрифти кешуються як будь-який інший статичний ресурс із довгостроковими заголовками кешу. Важливо знати: у next dev оптимізація не застосовується і шрифти завантажуються зовні. Повний ефект побачиш тільки після next build.
Основні параметри
| Параметр | Що робить | Приклад |
|---|---|---|
subsets | Які набори символів включити у завантаження | ['latin', 'cyrillic'] |
weight | Конкретні ваги для завантаження | ['400', '700'] |
display | Значення CSS font-display | 'swap' (рекомендовано) |
variable | Ім'я CSS-змінної | '--font-inter' |
preload | Додавати <link rel="preload"> | true (за замовчуванням) |
adjustFontFallback | Перевизначити fallback-шрифт для розрахунку size-adjust | 'Arial' |
axes | Увімкнути осі variable font | ['wght'] |
Типові помилки
1. Неправильний subset для нелатинського тексту
// Неправильно: завантажується тільки latin-набір гліфів
const inter = Inter({ subsets: ['latin'] });
// <p>Привет</p> рендериться як квадрати або взагалі не видноВиправлення: subsets: ['latin', 'cyrillic']. Subset визначає, які гліфи потрапляють у білд. Відсутні гліфи неможливо синтезувати на рантаймі.
2. display: 'block' для текстових шрифтів
// Неправильно: текст невидимий, поки шрифт не завантажиться (до 3с)
const inter = Inter({ display: 'block' });block говорить браузеру нічого не показувати, поки шрифт не прийде. Для основного текстового шрифту на повільному з'єднанні це кілька секунд порожнього контенту. Для текстових шрифтів використовуй 'swap' (за замовчуванням). 'block' залишай для іконкових шрифтів, де показати fallback-символ гірше, ніж нічого.
3. className на <html> замість <body>
// Неправильно: font-family на html не каскадується стабільно
<html className={inter.className}>
<body>{children}</body>
</html>className (стек font-family) іде на <body>. variable (CSS-змінна) іде на <html>, щоб дочірні елементи могли до неї звертатись. Правильний розподіл:
<html className={inter.variable}>
<body className={inter.className}>4. Неправильний шлях для локальних шрифтів
// Неправильно: шлях відносно кореня проєкту не спрацює
const font = localFont({ src: '../public/fonts/MyFont.woff2' });next/font/local розраховує шлях відносно файлу, де визначено шрифт, а не від кореня проєкту і не від public/. Клади WOFF2-файли в app/fonts/ поруч із layout, тоді:
const font = localFont({ src: './fonts/MyFont-Regular.woff2' });5. Відсутній axes для variable fonts
// Неправильно: завантажується тільки одна статична вага
const jetbrains = JetBrains_Mono({ subsets: ['latin'], weight: '400' });
// fontVariationSettings не інтерполюватиме інші ваги на рантайміДодай axes: ['wght'] щоб увімкнути повний діапазон ваг з одного WOFF2-файлу. Без цього Next.js завантажить тільки статичну вагу яку ти вказав, і змінна вісь нічого не дає.
Де зустрічається в реальних проєктах
- Сайт Vercel використовує Inter і SF Pro через
next/fontдля нульового CLS на маркетингових сторінках - Scaffolding від shadcn/ui одразу налаштовує Inter із CSS-змінною для Tailwind-теми
- create-t3-app використовує локальні шрифти для адмін-дашбордів
- Будь-який Next.js-проєкт, що цілиться у 90+ балів Lighthouse, потребує CLS нижче 0.1;
next/fontце забезпечує напряму
Питання на співбесіді
Q: Що робить font-display: swap?
A: Говорить браузеру одразу показувати fallback-шрифт, а потім замінити його на кастомний після завантаження. Заміна відбувається протягом перших 3 секунд (swap period). Без цього текст або невидимий (block), або залишається у fallback назавжди (fallback/optional).
Q: Яка різниця між className і variable?
A: className одразу вставляє декларацію font-family в елемент. variable створює CSS-змінну типу --font-inter, на яку посилаєшся в CSS або конфігу Tailwind. variable вибирають коли шрифти керуються через дизайн-систему; className підходить для швидкого застосування без зайвих шарів.
Q: Як size-adjust запобігає зсуву макета?
A: Next.js зчитує вертикальні метрики шрифту (ascent, descent, line-gap) під час збірки і вираховує відсоток масштабування fallback-шрифту, щоб розміри тексту збіглись із кастомним. Коли відбувається swap, розміри елементів не змінюються, тому CLS = 0.
Q: Чи працює next/font у Pages Router?
A: Так. В App Router конфігурується в app/layout.tsx, у Pages Router - в _app.tsx або _document.tsx. App Router переважніший, бо дає повноцінне preload-поведінку в <head>, але обидва варіанти працюють.
Q (senior): Користувач повідомляє про стрибки тексту на мобільному Safari. Ти трасуєш проблему до next/font. Як діагностуватимеш і виправлятимеш?
A: Спочатку відтворюю: запускаю Lighthouse у мобільному режимі в Chrome DevTools, перевіряю CLS-score і які елементи зсуваються. Потім перевіряю, чи є variable font із визначеними axes. iOS Safari до версії 15.4 має часткову підтримку осей variable fonts, тому ваги через fontVariationSettings можуть не інтерполюватися. Виправлення: вказати weight: ['100', '400', '700'] як статичний масив. Також перевіряю adjustFontFallback: на iOS дефолтний fallback може бути SF Pro з іншими метриками, ніж Arial. Перевизначаю через adjustFontFallback: 'Arial' для стабільного розрахунку size-adjust на всіх платформах. Відповідь джуна - "використовуй swap"; реальне виправлення вимагає відтворити проблему, перевірити підтримку осей і метрики fallback-шрифту.
Приклади
Google Fonts з інтеграцією Tailwind CSS
Продакшн-layout із двома Google Fonts, підключеними до Tailwind через CSS-змінні:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-sans',
});
const mono = Roboto_Mono({
subsets: ['latin'],
weight: '400',
display: 'swap',
variable: '--font-mono',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${mono.variable}`}>
<body className={inter.className}>
{children}
</body>
</html>
);
}// tailwind.config.ts
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)', 'system-ui'],
mono: ['var(--font-mono)', 'monospace'],
},
},
},
};Обидва шрифти завантажуються під час збірки як subsets. CSS-змінні потрапляють у Tailwind-утиліти font-sans і font-mono, тому будь-який компонент у дереві може використати className="font-sans" або className="font-mono" без повторного імпорту шрифту.
Локальний шрифт із кількома вагами
Коли є кастомний брендовий шрифт у форматі WOFF2, якого немає в Google Fonts:
// app/layout.tsx
import localFont from 'next/font/local';
const brand = localFont({
src: [
{ path: './fonts/Brand-Regular.woff2', weight: '400', style: 'normal' },
{ path: './fonts/Brand-Bold.woff2', weight: '700', style: 'normal' },
{ path: './fonts/Brand-Italic.woff2', weight: '400', style: 'italic' },
],
display: 'swap',
variable: '--font-brand',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={brand.variable}>
<body className={brand.className}>
{children}
</body>
</html>
);
}Файли шрифтів лежать у app/fonts/ поруч із layout.tsx. Next.js підхоплює їх під час збірки, копіює в output і генерує одне правило @font-face для кожної комбінації ваги і стилю. Ручні теги preload не потрібні.
Variable font з віссю ваг
Один WOFF2-файл може покрити всі ваги, якщо шрифт підтримує змінні осі:
// app/layout.tsx
import { JetBrains_Mono } from 'next/font/google';
const jetbrains = JetBrains_Mono({
subsets: ['latin'],
axes: ['wght'], // вмикає змінну вісь ваги
variable: '--font-code',
display: 'swap',
});// components/CodeBlock.tsx
export function CodeBlock({ children }: { children: React.ReactNode }) {
return (
<pre
className="font-code"
style={{ fontVariationSettings: '"wght" 500' }}
>
{children}
</pre>
);
}Без axes: ['wght'] Next.js завантажує один статичний файл і fontVariationSettings нічого не робить на рантаймі. З axes один variable WOFF2 покриває будь-яку вагу в підтримуваному діапазоні, і бандл менший ніж при завантаженні трьох-чотирьох окремих файлів ваг.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.