Разработка
Сайт студии быстрее секунды: наш бюджет производительности
Как мы делаем сайты студий, которые отрисовывают первый контент меньше чем за секунду на смартфоне среднего класса по 4G: бюджет, которого мы придерживаемся, от чего отказываемся и какие небольшие архитектурные решения важнее всего для Core Web Vitals.
В этой статье
Есть особый тип сайтов студий — тех, где каждый скролл запускает мерцающий параллакс, смену шрифта, автоматически стартующий шоурил, который три секунды буферизуется, и курсор, у которого при наведении отрастают клыки. Такой сайт грузится восемь секунд на смартфоне среднего класса и кажется сломанным на любом соединении, кроме оптоволокна. Мы такие не делаем. Наше требование к себе жёсткое и простое: первая отрисовка контента меньше чем за секунду на Pixel 6a при ограниченном 4G-соединении.
Уложиться в это требование помогает не героическая оптимизация, а несколько архитектурных решений, эффект которых складывается, и куда более длинный список того, чего мы не делаем. В этой статье — бюджет, по которому мы работаем, решения, на которых он стоит, и полевые замеры, подтверждающие, что бюджет соблюдён. Если вы делаете сайт студии, агентства или любое портфолио, это реальный ориентир, который можно перенять почти целиком.
Бюджет в цифрах
Каждый сайт студии, который мы выпускаем, нацелен на эти цифры на Pixel 6a с ограничением Fast 3G в Chrome DevTools. На реальном 4G всё быстрее; Fast 3G мы берём как худший случай. Замеряем в Lighthouse, а в продакшне — по метрикам реальных пользователей через Vercel Analytics.
- Largest Contentful Paint (LCP): меньше 1,2 с в лаборатории и меньше 1,8 с (p75) в реальных условиях. Порог «хорошо» у Google — 2,5 с.
- First Contentful Paint (FCP): меньше 1,0 с в лаборатории.
- Cumulative Layout Shift (CLS): меньше 0,05. Порог «хорошо» у Google — 0,1; мы держим планку вдвое строже.
- Interaction to Next Paint (INP): меньше 100 мс. Порог «хорошо» у Google — 200 мс.
- Общий объём передачи при первой загрузке: меньше 150 КБ в сжатом виде, включая HTML, CSS, шрифты, JS и hero-изображение.
- JavaScript-бандл при первой загрузке: меньше 80 КБ в сжатом виде.
Если страница не укладывается хотя бы в один показатель, для нас это баг P1. Не «вернёмся к этому потом», а P1, который мы исправляем до запуска сайта.
Почему так строго?
Причин две. Во-первых, Core Web Vitals — фактор ранжирования в Google. Не довесок на случай равенства, а именно фактор ранжирования. Сайты, которые укладываются в пороги, могут получить преимущество за удобство страниц; остальные понемногу теряют позиции. Для студии, которая конкурирует за узкие кластеры ключевых слов, это «понемногу» имеет значение.
Во-вторых, воспринимаемая скорость — сигнал бренда. Сайт, который загружается мгновенно, производит впечатление профессионального. Сайт, который зависает, пока подгружается пиксель отслеживания, — нет. Если вся суть предложения студии — «мы делаем продуманные, современные проекты», сайт должен доказать это с первого касания. Сайт и есть бриф.
Решение 1. Всё рендерим статически
Самое важное решение для производительности — стратегия рендеринга. Мы используем Next.js с App Router, и каждая страница наших сайтов студий рендерится статически на этапе сборки. Никаких запросов к базе данных во время работы. Никакой серверной работы на каждый запрос, кроме edge-функции, которая отдаёт HTML. Весь сайт — это файлы на CDN.
Что это даёт на практике: TTFB на edge определяет ваш CDN — обычно 30–80 мс в любой точке планеты. Первый байт HTML приходит раньше, чем браузер успел бы завершить TLS-рукопожатие в варианте с серверным рендерингом. Всё, что дальше, — отрисовка, интерактивность, следующая страница — начинается настолько же раньше.
Цена этого — любое изменение контента требует деплоя. Для сайта студии, который обновляется раз в месяц, деплой — не событие: на Vercel он занимает 90 секунд. Для издания, которое обновляется каждый час, расчёт другой, и правильный инструмент — ISR (Incremental Static Regeneration). Но сайты студий не обновляются каждый час.
Решение 2. Один веб-шрифт, два начертания
Кастомные шрифты — самая крупная из контролируемых статей расходов при первой отрисовке. Подмножество веб-шрифта в одном начертании обычно весит 25–40 КБ в сжатом виде. Сайт, который грузит по три начертания двух гарнитур, отправляет 150 КБ шрифтов ещё до того, как отрисуется первый байт контента. Мы используем одну гарнитуру в двух начертаниях с собственным хостингом и хешированием через next/font — без стороннего запроса к Google Fonts и без DNS-запроса к CDN шрифтов.
И мы принимаем последствия: ни курсива, ни тонкого, ни сверхжирного. Любое типографическое различие приходится строить на размере, цвете, трекинге или контрасте между двумя имеющимися начертаниями. Это творческое ограничение, которое окупает себя. Сайты, которым нужно восемь начертаний, обычно прячут слабую иерархию за типографическим разнообразием; сайты с двумя начертаниями вынуждены выстраивать иерархию структурно.
Решение 3. Бюджет hero-изображения и как его тратить
На сайте студии LCP почти всегда приходится на hero-блок. Значит, бюджет LCP — это бюджет hero. Мы задаём ему жёсткий потолок: 80 КБ в сжатом виде на десктопном брейкпоинте и 40 КБ на мобильных. Всё, что больше, — творческая задача, которую нужно решить выбором или обработкой изображения, а не повод выйти за рамки бюджета.
Четыре практики, которые помогают уложиться в бюджет:
- Используйте AVIF там, где он поддерживается, с WebP в качестве запасного варианта. При равном качестве AVIF обычно на 30–40% легче WebP.
- Отдавайте адаптивные размеры через компонент next/image. Телефоны никогда не скачивают десктопный hero, а десктопы не скачивают версию retina-2x, если экран на самом деле не retina.
- Задавайте явные width и height для каждого изображения. Именно это предотвращает сдвиг макета: браузер резервирует нужное количество пикселей ещё до загрузки картинки.
- Предзагружайте hero-изображение: <link rel="preload" as="image" href="…"/> в head. Одна строка экономит 200–400 мс на LCP, потому что браузер запрашивает изображение ещё до разбора остальной страницы.
Решение 4. JavaScript — в последнюю очередь
Каждый килобайт JavaScript нужно скачать, разобрать и выполнить в основном потоке, прежде чем страница станет интерактивной. Мы считаем бюджет на JS дефицитным и придерживаемся двух правил:
- Серверные компоненты по умолчанию. Помечайте компонент 'use client', только если ему действительно нужна интерактивность. Поведение по умолчанию в React Server Components — правильный выбор для контентных сайтов.
- Никаких сторонних скриптов при первой загрузке. Никакой аналитики, которая тянет 80 КБ чужого кода. Никаких менеджеров тегов. Аналитика загружается лениво, когда страница уже интерактивна: мы используем Vercel Analytics, это около 1 КБ.
Для наших сайтов студий бюджет — меньше 80 КБ сжатого JS при первой загрузке, и большая часть из них — сам React. Интерактивный слой (меню навигации, переключатель языка, контактная форма) занимает несколько килобайт.
Решение 5. CSS-архитектура без сдвигов макета
Cumulative Layout Shift — метрика Core Web Vitals, которую проще всего провалить случайно и проще всего исправить намеренно. Четыре привычки, которые держат CLS около нуля:
- Резервируйте место под всё, что приходит с опозданием. Изображения, шрифты, iframe, реклама — всё получает контейнер с aspect-ratio и явными размерами.
- Избегайте CSS, который зависит от загрузки шрифта. На время подмены используйте системные запасные шрифты с font-display: swap. Выбирайте запасной шрифт с метриками, близкими к веб-шрифту, чтобы подмена была незаметной.
- Не вставляйте контент на первый экран после загрузки. Баннеры, уведомления о cookie и строки объявлений, появляющиеся после первой отрисовки, — это сдвиг макета, который только и ждёт своего часа. Если они нужны, рендерьте их в исходном HTML и проявляйте анимацией.
- Тестируйте на медленных сетях. Проблемы с CLS, незаметные на оптоволокне, проявляются на 3G, потому что запаздывающий ресурс приходит ещё позже.
Решение 6. Бюджет на анимацию
Сдержанность в движении — сама по себе оптимизация производительности. Любая анимация в основном потоке грозит ухудшить INP: одна дёрганая анимация на 250 мс по клику выбрасывает весь сайт из категории «хорошо» по INP. Наши правила:
- Анимируйте только transform и opacity. Оба свойства композитятся на GPU и не вызывают перерасчёт макета.
- Анимации появления — на CSS, а не на JavaScript. IntersectionObserver добавляет className, а сама анимация — это transition.
- Никаких анимаций, привязанных к скроллу. Звучит элегантно, а на устройствах среднего класса даёт ужасную производительность прокрутки.
- Никакого автовоспроизведения видео в hero-блоке. Если видео нужно, загружайте его лениво ниже первого экрана или по действию пользователя.
Решение 7. Доступность и производительность заодно
Большинство улучшений доступности — заодно и улучшения производительности. Семантический HTML легче, чем «суп из div». Нативные рамки фокуса отрисовываются быстрее, чем кастомное управление фокусом на JS. Ссылки для пропуска навигации в шапке избавляют от исправлений ловушек клавиатуры, которые тянут за собой JavaScript. Мы проектируем и разрабатываем с прицелом на доступность, а производительность получаем бесплатно.
Единственное место, где производительность и доступность вступают в конфликт, — анимация. Одним пользователям движение нужно, других от него укачивает. Мы учитываем prefers-reduced-motion, и у каждой анимации появления есть запасной вариант — плавное проявление, а не смещение.
Замеры в продакшне
Лабораторные метрики Lighthouse необходимы, но недостаточны. Золотой стандарт — мониторинг реальных пользователей (RUM): то, что видят настоящие посетители на настоящих устройствах. Для Web Vitals мы используем Vercel Analytics: он бесплатно входит в платформу и показывает полевые метрики в разрезе маршрутов, устройств и географии.
Что мы смотрим каждую неделю:
- p75 LCP по каждому маршруту. Если у какого-то маршрута p75 превышает 1,8 с, разбираемся.
- Ухудшения INP. INP — самая коварная метрика: она может незаметно проседать по мере роста JavaScript.
- CLS по маршрутам. Ухудшения CLS обычно сводятся к одному элементу, который загружается поздно.
- JS при первой загрузке по маршрутам — отслеживаем анализатором бандла в CI. Сборка, превысившая бюджет, у нас падает.
Чего мы не делаем
Не менее важно то, от чего мы сознательно отказываемся, хотя это стандарт индустрии:
- Никакой клиентской маршрутизации в стиле SPA на небольшом сайте. На 4G загрузить документ целиком быстрее, чем дельту бандла. Навигацию мы оставляем браузеру.
- Никакого service worker. На сайте меньше чем из 10 страниц сложность себя не оправдывает.
- Никаких сервисов хостинга изображений, которые встраивают SDK на 30 КБ. Мы отдаём изображения с того же домена, что и сайт, оптимизированными на этапе сборки.
- Никаких «умных» библиотек ленивой загрузки. Нативный атрибут loading="lazy" справляется с этим за 0 КБ.
- Никакой предзагрузки каждой ссылки при наведении. Next.js в продакшне и так делает prefetch по умолчанию; мы оставляем это включённым и доверяем решение фреймворку.
Если вы планируете сайт и хотите аудит производительности того, что уже есть, или сайт, с самого начала построенный под этот бюджет, — hello@neptay.com.