dbit.one© 2026
000
booting_
Loading experience0%
dbit.one

LCP 6,4 секунды из-за заслонки, которую никто не видел

Коротко: разбор нашего собственного сайта. Текст первого экрана приезжал с сервера за 10 миллисекунд, а появлялся на экране через шесть с половиной секунд — его закрывала интро-заставка, снять которую мог только загрузившийся JavaScript. После починки LCP 6,4 → 2,2 с, оценка производительности 73 → 98. Ниже — как искали, включая гипотезу, которая оказалась неверной.

7 мин чтения

Коротко

  • Готовый HTML ничего не гарантирует: LCP считается по моменту отрисовки, а не по моменту доставки.
  • Самая дорогая ошибка была не в анимации, а в том, что условие «показывать ли интро» жило в React-хуке — то есть становилось известно только после гидратации.
  • Первая гипотеза (начальные стили framer-motion в разметке) была верной, но второстепенной: сама по себе она не дала ничего.
  • Итог на одном и том же билде: LCP 6,4 → 2,2 с, Speed Index 4,9 → 1,9 с, оценка 73 → 98.

С чего началось

Скорость загрузки — тот пункт, на котором нельзя экономить при разработке сайта, и мы пишем это клиентам. Поэтому начали с себя: прогнали Lighthouse по собственному сайту — не по клиентскому, по своему. Мобильная оценка производительности показала 76 при почти идеальных остальных метриках: время до первого байта полсекунды, блокировка главного потока 60 мс, сдвиг вёрстки 0,001. И LCP 7,0 секунды.

Сочетание странное. Обычно плохой LCP идёт в компании с плохим TBT: тяжёлый JavaScript душит и то и другое. Здесь главный поток был свободен, картинок в первом экране нет, шрифты грузились со swap. Разбивка LCP объясняла всё одной строкой: время до первого байта — 0,55 с, остальные 6,4 секунды — «element render delay». Элемент был доставлен и не отрисован.

Первая гипотеза — и почему она не сработала

В разметке первого экрана обнаружилось 95 элементов с opacity: 0, включая сам LCP-элемент — абзац под заголовком. Причина понятная: framer-motion записывает начальное состояние анимации прямо в атрибут style при серверном рендере. Пока React не загрузится и не запустит анимацию, текст физически невидим.

html
<p class="max-w-xl text-lg leading-relaxed text-ink-dim"
   style="opacity:0;transform:translateY(20px)">
  От лендинга до онлайн-банка — один партнёр на весь цикл…
</p>
Так выглядел LCP-элемент в HTML, приехавшем с сервера. Текст есть — увидеть его нельзя.

Объяснение выглядело исчерпывающим, и мы переписали анимацию первого экрана на CSS: те же кадры, та же кривая, та же длительность — но начинается она с первым кадром отрисовки, а не после гидратации. Собрали, замерили.

Ничего не изменилось. LCP как был 5,0 с на локальном билде, так и остался 5,0 с. Это и есть главный момент разбора: правдоподобное объяснение и правильное объяснение — разные вещи, и различает их только замер.

Гипотеза, которая объясняет симптом, но не исчезает вместе с ним после починки, — не причина, а совпадение.

Настоящая причина

Дальше мы посмотрели не на стили LCP-элемента, а на то, что находится над ним. В разметке нашлась интро-заставка — та самая, с бегущими цифрами, которая на десктопе создаёт настроение при входе:

html
<div class="fixed inset-0 z-[1000] flex flex-col justify-between bg-base p-6">
  …  <!-- 000  ·  booting  ·  Loading experience -->
</div>

Непрозрачный слой на весь экран, поверх всего. На телефоне интро показывать не нужно, и код это учитывал — условие проверял хук useCanRender3D(): десктоп, точный указатель, достаточно ядер и памяти. Проблема в том, когда это условие становится известно. Хук — это React, React — это гидратация. До неё сервер не знает, кто на другом конце, и рендерит заставку всем.

БылоHTML готовЗагрузка и разбор JSГидратация6.4sСтало2.2s · Текст виденТекст всё это время лежал в разметке — под заслонкой
Текст первого экрана был получен браузером почти сразу. Всё остальное время он лежал под непрозрачным слоем, снять который мог только загрузившийся и отработавший JavaScript.

Получалась замкнутая конструкция: чтобы убрать заглушку, нужен JS; пока его нет, пользователь смотрит на пустой тёмный экран, хотя весь контент уже у него. Заодно объяснилось, почему первая гипотеза не помогла: прозрачность абзаца не имела значения — его всё равно закрывали сверху.

Починка

Решение — перенести условие показа из JavaScript в CSS. Медиазапрос вычисляется при первом же расчёте стилей, до всякого React, и на телефоне заставка просто не существует как нарисованный слой.

css
@media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) {
  .intro-gate {
    display: none !important;
  }
}
Условие то же самое, что в хуке. Разница только в том, что оно применяется с первого кадра.

Вторая часть — та самая переписанная на CSS анимация первого экрана. Сама по себе она ничего не дала, но после снятия заслонки стала обязательной: убрав верхний слой, мы бы упёрлись ровно в opacity: 0 следующим шагом. Две ошибки маскировали друг друга.

Что именно изменилось
  1. 01

    Заставка спрятана медиазапросом

    Слой перестал рисоваться на устройствах, где интро не показывают, — независимо от того, загрузился ли JavaScript.

  2. 02

    Анимация первого экрана переехала на CSS

    Кадры, длительности и кривая прежние. Разница только в моменте старта: первый кадр отрисовки вместо конца гидратации.

  3. 03

    Ничего не удалено

    На десктопе интро идёт как шло. Это важно: фирменная анимация — часть продукта, а не то, чем расплачиваются за метрику.

Результат

Замеры сделаны на одной машине, на одном и том же коде, с реальным троттлингом (Lighthouse, мобильный профиль, --throttling-method=devtools), локальный прод-билд — чтобы исключить сеть и разброс.

Метрика
Было → стало
Оценка производительности
73 → 98
Largest Contentful Paint
6,4 с → 2,2 с
Speed Index
4,9 с → 1,9 с
First Contentful Paint
1,8 с → 1,6 с
Element render delay
6394 мс → 2186 мс

Как проверить это у себя

Пять минут и никаких инструментов, кроме браузера
  • Откройте исходный код страницы (именно view-source, а не инспектор — инспектор показывает состояние после JS) и найдите текст своего заголовка. Если его там нет, разговор про LCP преждевременен: сначала серверный рендер.
  • Найдите в этом же HTML opacity:0, visibility:hidden и transform: translate рядом с контентом первого экрана. Всё это — невидимый текст.
  • Найдите элементы с position: fixed и высоким z-index, которые рендерятся всегда, а скрываются по условию из JS. Заставки, прелоадеры, баннеры согласия.
  • Прогоните Lighthouse с --throttling-method=devtools и посмотрите разбивку LCP. Большой element render delay при маленьком TTFB — это ваш случай.
  • Проверьте, не живёт ли условие показа чего-то крупного в React-хуке. Всё, что можно решить медиазапросом, должно решаться медиазапросом.

Общее правило, которое мы из этого вынесли: всё, что закрывает контент, должно уметь исчезать без JavaScript. Если снять слой может только скрипт, то время его загрузки становится временем появления вашего контента — со всеми последствиями для поведения людей и для ранжирования.

Частые вопросы

Почему просто не убрать интро совсем?

Потому что на десктопе оно работает: это часть впечатления от продукта, и мы не считаем правильным платить продуктом за метрику. Задача была не «убрать анимацию», а «сделать так, чтобы она не мешала тем, кому её не показывают». После починки на десктопе не изменилось ничего.

Это проблема Next.js или framer-motion?

Ни то, ни другое. Оба инструмента делают ровно то, что обещают. Ошибка архитектурная: условие, от которого зависит видимость контента, оказалось доступно только после гидратации. Такое повторяется на любом стеке с серверным рендером — React, Vue, Svelte без разницы.

Насколько это вообще важно для бизнеса?

LCP — один из трёх Core Web Vitals, а они входят в сигналы ранжирования Google и считаются по реальным пользователям, в основном мобильным. Но ещё до всякого поиска: шесть секунд тёмного экрана — это люди, которые ушли обратно в выдачу, не увидев ни строчки. Деньги теряются раньше, чем позиции.

Почему вы публикуете разбор собственной ошибки?

По двум причинам. Во-первых, эту ошибку легко повторить, а описания её мы не нашли — так что материал полезен. Во-вторых, разбор своего промаха с числами проверяется за пять минут: откройте наш сайт и прогоните Lighthouse сами. Кейс, который можно проверить, стоит дороже кейса, который нужно принять на веру.

Источники

Утверждения из статьи можно проверить: ниже первоисточники, а не пересказ.

  1. 01web.dev — Largest Contentful Paint (LCP)определение метрики и разбор её на составляющие, включая element render delay
  2. 02Chrome for Developers — Lighthouse performance scoringкак считается итоговая оценка и чем отличаются режимы троттлинга
Инженерная редакция dbit.one
Инженеры, которые строят эти системы

Материалы пишут инженеры, работающие на проектах, — но без подписи именем. Причина та же, по которой на сайте нет логотипов клиентов: почти все проекты идут под NDA или white-label, и авторство статьи о платёжном ядре указывает на заказчика не хуже логотипа. Взамен мы отвечаем за текст правилами, а не именами — кроме материалов о собственном открытом коде: те подписаны автором.

Как мы пишем и что проверяем

Услуги по теме

Читать дальше

Руководство8 мин чтения

Разработка финтех-продукта: полное руководство

Коротко: финтех отличается от обычной разработки не технологиями, а ценой ошибки. Здесь нельзя «потом поправим»: неверная цифра на счёте — это не баг, а претензия. Ниже — из чего состоит такой продукт, что требует регулятор, где закапывают бюджет и сколько это стоит на самом деле.

Цены и сроки4 мин чтения

Сколько стоит корпоративный сайт компании

Коротко: корпоративный сайт с уникальным дизайном, редактируемым контентом и заявками стоит от $12 000 и занимает 5–8 недель. Лендинг обойдётся от $6 000, портал с личными кабинетами — от $28 000.

Инженерия10 мин чтения

Тесты не доказывают отсутствие бага. Что доказывает

Коротко: набор тестов — это поиск. Он умеет сказать «нашёл» и не умеет сказать «нет». Для большей части кода этого достаточно, но не для протокола оплаты, конечного автомата заказа и любого места, где две задачи трогают общее состояние: там редкое сочетание шагов и есть баг. Проверка моделей обходит все достижимые состояния и отвечает на вопрос, на который тесты не отвечают в принципе.

Остались вопросы по вашему проекту?

Опишите задачу — в течение 24 часов вернёмся с оценкой, сроками и планом.

[email protected]