Skip to main content
Назад к блогу
6 мин чтения

Как я выбил Lighthouse 98 на трёхъязычном сайте на Next.js

Оптимизации, которые реально подвинули баллы, две, на которые я потратил время зря, и то, о чём большинство Lighthouse-туториалов тихо врут.

next.jsperformancei18n

Этот сайт (тот, который ты читаешь) набирает 98 баллов мобильного Lighthouse в продакшне, на трёх локалях, с реальным блогом, реальным роутингом и реальной контактной формой. Я не отгрузил статичный HTML-клон ради этого. Вот что реально сдвинуло число, что не сдвинуло, и что бы я пропустил в следующий раз.

Перед чем-либо: балл Lighthouse — это прокси, а не цель. Цель — чтобы сайт грузился быстро для человека на 4G-соединении в лобби отеля. Если оптимизация ради балла делает сайт медленнее для него — я игнорирую балл. Упоминаю, потому что большинство гайдов «как добить до 100» оптимизируют ради скриншота, а не ради пользователя.

Коротко — что сдвинуло число

По порядку влияния на моих собственных аудитах:

  1. Самохостинг шрифтов, подрезка до глифов, которые реально использую
  2. Замена каждого декоративного PNG/JPG на inline SVG
  3. Измерение какой клиентский JS отгружается и убийство половины
  4. Оплата i18n-налога ровно один раз, на этапе сборки
  5. Tailwind 4 + tree-shaken CSS, без глобального reset-кухонного мусора

Это стек-ранкинг. Последние два часто пропускают в гайдах, и они стоят гораздо больше, чем большинство советов по оптимизации картинок.

1. Шрифты — самый большой бесплатный выигрыш

Google Fonts через <link> — единственная самая частая перф-ошибка, которую я вижу на современных сайтах. Даже с font-display: swap цепочка запросов такая:

  1. HTML загружается
  2. Браузер парсит, находит <link> на fonts.googleapis.com
  3. DNS, TLS, фетч CSS
  4. Парс, нахождение реальных URL .woff2 (другой домен)
  5. DNS, TLS, фетч шрифтов
  6. Рендер

Пять round-trip'ов до отрисовки текста. На медленном 4G это 800ms+ пустого экрана до LCP.

Что делаю я: next/font/local, файлы .woff2 в /public/fonts/, подрезанные до Latin + Latin Extended + Cyrillic. font-display: swap. Один запрос, тот же origin, агрессивно кешировано.

Код:

// app/fonts.ts
import localFont from 'next/font/local'

export const inter = localFont({
  src: [
    {
      path: './fonts/Inter-Variable.woff2',
      weight: '100 900',
      style: 'normal',
    },
  ],
  variable: '--font-inter',
  display: 'swap',
  preload: true,
})

Подрезать .woff2 с помощью pyftsubset или glyphhanger, чтобы выбросить CJK, арабские и математические глифы, которые никогда не используешь. Вариативный шрифт, который был 480KB, стал 86KB после подрезки до трёх скриптов, которые мне реально нужны.

Дельта баллов: ~+8 на мобильном.

2. SVG для каждого декоративного изображения

Каждая иконка, каждая иллюстрация, каждый акцент на этом сайте — inline SVG или SVG из /public. Ноль декоративных PNG/JPG. Фото подаются через next/image с AVIF + responsive srcset; просто у меня мало фотографий.

Почему это bigger deal, чем признают гайды по сжатию картинок: SVG — часть HTML-потока. Они рисуются вместе с остальной страницей. Растровые изображения — это второй запрос, второй декод, и они блокируют LCP, пока не приземлятся.

Цена: вечер в Figma на экспорт чистых SVG и ещё час на оптимизацию с SVGO. Стоило каждой минуты.

Дельта баллов: ~+5 на мобильном, но больший выигрыш — в ощущаемой отзывчивости: страница визуально завершается за один paint вместо двух.

3. Честный аудит клиентского JS

Штука, которая смиряет: открой DevTools Coverage tab, загрузи свой сайт, поскролль немного и посмотри, сколько JS ты отгрузил vs сколько реально выполнилось.

Мой первый аудит на этом сайте: 180KB JS отгружено клиенту, ~40KB реально выполнено на first paint. 140KB неиспользованных байтов висят в network tab просто так.

Откуда это шло:

Паттерн фикса для каждого:

// framer-motion — использовать lazy-loaded вход
import { LazyMotion, domAnimation, m } from 'framer-motion'

// syntax highlighter — грузить только когда блок кода виден
const SyntaxHighlighter = dynamic(() => import('react-syntax-highlighter'), {
  ssr: false,
})

// иконки — импортировать четыре, которые использую, а не бочку
import ShieldIcon from '@/components/icons/ShieldIcon' // inline SVG, 200 байт

Снизил до ~55KB отгруженных после аудита, ~40KB выполненных. Оставшиеся 15KB — роутинг + гидратация, неизбежные без отказа от Next.

Дельта баллов: ~+6, но важнее — ~400ms снижения Time to Interactive.

4. i18n-налог, оплаченный один раз на этапе сборки

Этот сайт рендерится на английском, русском и украинском. Наивный подход — отгружать все три словаря переводов на каждую страницу и выбирать один в рантайме. Это ~90KB JSON ради фичи, которая обслуживает один язык за раз.

next-intl (библиотека, которую я использую) поддерживает статическую загрузку по локали на этапе сборки с App Router. Каждая локаль компилирует свой собственный статический HTML только со своими сообщениями. JSON на локаль в клиентском бандле: ~8KB, не 90KB.

// i18n/request.ts
export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`../messages/${locale}.json`)).default,
}))

Трюк в шаблонном литерале. Если написать await import('../messages/en.json') статически — бандлер отгружает только этот. Если с динамической переменной — Next инлайнит по-локально на этапе сборки, потому что роутинг — по-локальный. Одна локаль, один файл сообщений, нет переключения на клиенте.

Дельта баллов: не огромное число на Lighthouse (может +2 из-за уменьшения бандла), но это разница между сайтом, который ощущается быстрым, и сайтом, который ощущается раздутым для пользователей, открывающих DevTools.

5. Tailwind 4, без глобального reset-мусора

Tailwind 4 с @theme и @utility отгружает долю CSS Tailwind 3, при правильном использовании. Продакшн-бандл этого сайта — ~9KB CSS (gzipped), и включает небольшую кастомную тему, полную библиотеку компонентов и типографику для MDX.

Две конкретные вещи:

Дельта баллов: маленькая на Lighthouse напрямую, но каждый KB CSS — это KB, задерживающий first paint на медленных соединениях.

Что не помогло

Две вещи, на которые я потратил время и которые сдвинули балл меньше чем на пункт:

Edge-рендеринг всего. Попробовал переместить серверные компоненты на edge runtime на Vercel. Реальное улучшение латенси: 30ms в среднем, редко попадая на критический путь. И потерял доступ к некоторым Node-only API, которые не хотел рефакторить. Откатил.

Ручная тюнинг critical CSS extraction. Попробовал ручной critical-CSS экстрактор, который инлайнил above-the-fold стили в <head> и откладывал остальное. Технически работало — но с Tailwind 4, уже производящим 9KB CSS, critical fold по сути был всем файлом. Добавил сложность ради 0.2 очка улучшения.

Если CSS 80KB+ на странице, critical extraction вероятно стоит того. Ниже 15KB — пропускай.

Что бы я сделал по-другому

Если бы стартовал этот сайт с нуля завтра, одна вещь, которую бы сделал раньше: замерять с первого дня. Настроить Lighthouse CI проверку в GitHub Actions, ронять сборку ниже порога, смотреть трейс на каждом PR. Поздние выигрыши на этом сайте пришли после того, как у меня появилась инфраструктура, чтобы видеть регрессии — которую я настроил на два месяца позже, чем стоило.

Остальные оптимизации — маленькие выигрыши, аккуратно сложенные. Нет одного трюка. Если ты ниже 90 — начни со шрифтов. Если застрял на 92–94 — аудить свой JS. Если на 95+ и гонишься за последними тремя очками — отдача на усилие падает жёстко.


Если хочешь перф-аудит своего Next.js или React Native приложения — я делаю разовые аудиты (€500, оборот 3 дня, письменный отчёт). Укажи в контактной форме, и я пришлю дату.