Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Оптимізація шрифтів (next/font) у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`next/font`** - це вбудована система у Next.js, яка хостить шрифти під час збірки, прибирає зовнішні запити та усуває CLS (зсув макета) через автоматичний `size-adjust`. ```tsx import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter' }); // Під час збірки: WOFF2 хоститься локально, @font-face вбудовано в CSS ``` **Ключове:** `next/font` замінює `<link href="fonts.googleapis.com">` на self-hosted шрифти без CLS.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`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.css - `display: 'swap'` показує fallback-шрифт миттєво, потім замінює його на кастомний після завантаження - Для будь-якого шрифту в продакшн Next.js-проєкті використовуй `next/font`; пропускай тільки для статичних сайтів без JS, де CLS не критичний ### Швидкий приклад ```tsx // 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 для нелатинського тексту** ```tsx // Неправильно: завантажується тільки latin-набір гліфів const inter = Inter({ subsets: ['latin'] }); // <p>Привет</p> рендериться як квадрати або взагалі не видно ``` Виправлення: `subsets: ['latin', 'cyrillic']`. Subset визначає, які гліфи потрапляють у білд. Відсутні гліфи неможливо синтезувати на рантаймі. **2. `display: 'block'` для текстових шрифтів** ```tsx // Неправильно: текст невидимий, поки шрифт не завантажиться (до 3с) const inter = Inter({ display: 'block' }); ``` `block` говорить браузеру нічого не показувати, поки шрифт не прийде. Для основного текстового шрифту на повільному з'єднанні це кілька секунд порожнього контенту. Для текстових шрифтів використовуй `'swap'` (за замовчуванням). `'block'` залишай для іконкових шрифтів, де показати fallback-символ гірше, ніж нічого. **3. `className` на `<html>` замість `<body>`** ```tsx // Неправильно: font-family на html не каскадується стабільно <html className={inter.className}> <body>{children}</body> </html> ``` `className` (стек `font-family`) іде на `<body>`. `variable` (CSS-змінна) іде на `<html>`, щоб дочірні елементи могли до неї звертатись. Правильний розподіл: ```tsx <html className={inter.variable}> <body className={inter.className}> ``` **4. Неправильний шлях для локальних шрифтів** ```tsx // Неправильно: шлях відносно кореня проєкту не спрацює const font = localFont({ src: '../public/fonts/MyFont.woff2' }); ``` `next/font/local` розраховує шлях відносно файлу, де визначено шрифт, а не від кореня проєкту і не від `public/`. Клади WOFF2-файли в `app/fonts/` поруч із layout, тоді: ```tsx const font = localFont({ src: './fonts/MyFont-Regular.woff2' }); ``` **5. Відсутній `axes` для variable fonts** ```tsx // Неправильно: завантажується тільки одна статична вага 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-змінні: ```tsx // 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> ); } ``` ```ts // 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: ```tsx // 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-файл може покрити всі ваги, якщо шрифт підтримує змінні осі: ```tsx // app/layout.tsx import { JetBrains_Mono } from 'next/font/google'; const jetbrains = JetBrains_Mono({ subsets: ['latin'], axes: ['wght'], // вмикає змінну вісь ваги variable: '--font-code', display: 'swap', }); ``` ```tsx // 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 покриває будь-яку вагу в підтримуваному діапазоні, і бандл менший ніж при завантаженні трьох-чотирьох окремих файлів ваг.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.