INP на лендинге Next.js 15: framer-motion без просадки Core Web Vitals
Google с 2024 года оценивает Interaction to Next Paint (INP) как один из трёх Core Web Vitals. Для лендинга это критично: первый клик по кнопке «Записаться», раскрытие FAQ или переключение таба тарифов должен откликаться быстрее 200 мс — иначе страница теряет «хороший» статус в Search Console, а конверсия падает на 5–15% по данным полевых исследований.
Проблема в том, что красивый лендинг почти всегда тянет framer-motion: hero с fade-in, staggered-карточки преимуществ, layout-анимации аккордеона. Каждый motion-компонент — это JavaScript на клиенте. Если весь hero помечен 'use client', React гидратирует тяжёлый бандл до первого взаимодействия, и INP взлетает.
В этой статье разберём production-подход: App Router Next.js 15, shadcn/ui для интерактивных элементов, framer-motion только там, где он нужен, и замеры через Vercel Speed Insights. Цель — анимированный лендинг с INP ≤ 200 мс на мобильных устройствах среднего сегмента.
Почему INP важнее FID для SEO-лендинга
First Input Delay (FID) измерял только задержку до начала обработки первого события. INP смотрит на все взаимодействия за сессию и берёт худший перцентиль (или почти худший — 98-й). Для лендинга с формой, FAQ и CTA это означает: не спасёт «быстрый первый клик», если второй — раскрытие аккордеона — тормозит на 400 мс.
| Метрика | Что измеряет | Порог «хорошо» | Типичная проблема лендинга |
|---|---|---|---|
| LCP | Скорость отрисовки главного контента | ≤ 2,5 с | Тяжёлый hero-image без priority |
| INP | Задержка отклика на взаимодействие | ≤ 200 мс | Блокирующий JS от framer-motion |
| CLS | Стабильность вёрстки | ≤ 0,1 | Layout-анимации без резервирования высоты |
INP складывается из трёх фаз: input delay (очередь задач на main thread), processing time (обработчики React) и presentation delay (перерисовка). Framer-motion бьёт по всем трём: регистрирует listeners, запускает spring-расчёты на каждый frame, триггерит layout reflow.
Для SEO это не абстракция. Search Console показывает INP в отчёте Core Web Vitals; страницы с «Needs improvement» или «Poor» теряют преимущество в Page Experience. На конкурентных запросах («разработка сайтов», «запись к стоматологу») разница в 100 мс INP коррелирует с позицией и CTR.
Как framer-motion ухудшает INP — и когда это неизбежно
Framer-motion добавляет в бандл motion-компоненты, gesture-движки и layout projection. Типичная ошибка на лендинге — обернуть всю страницу в 'use client' ради одной анимации заголовка:
// ❌ Плохо: весь hero — клиентский компонент
'use client';
import { motion } from 'framer-motion';
export function Hero() {
return (
<motion.section initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<h1>Разработка сайтов под ключ</h1>
<p>Срок — 14 дней. Core Web Vitals в зелёной зоне.</p>
<Button>Обсудить проект</Button>
</motion.section>
);
}
Пока браузер не скачает и не выполнит ~30–45 KB gzip framer-motion (плюс React client runtime), кнопка визуально есть, но не откликается. Input delay растёт.
Правильная модель для Next.js 15 App Router:
- Статический HTML hero отдаётся из Server Component — заголовок, текст, CTA видны роботу и пользователю сразу.
- Анимация — тонкая client-обёртка вокруг декоративного слоя, не вокруг интерактивных элементов.
- Интерактив (кнопки, формы shadcn/ui) — отдельные client-компоненты с минимальным JS.
// ✅ HeroStatic — Server Component (page.tsx)
import { HeroAnimation } from './hero-animation';
import { BookingButton } from './booking-button';
export function HeroStatic() {
return (
<section className="relative">
<HeroAnimation /> {/* только декоративный fade */}
<h1>Разработка сайтов под ключ</h1>
<p>Срок — 14 дней. Core Web Vitals в зелёной зоне.</p>
<BookingButton /> {/* shadcn/ui, лёгкий client */}
</section>
);
}
Layout-анимации (layout, layoutId) — главный враг INP на FAQ и табах. Каждое переключение пересчитывает геометрию всех motion-потомков. Для SEO-блока FAQ лучше CSS grid-template-rows: 0fr → 1fr или Radix Accordion из shadcn/ui без layout projection.
Если нужна подобная разработка — напишите в Telegram.
Архитектура: границы клиента и lazy loading
Разделение на server shell и client islands — тот же принцип, что в Astro Islands, но в Next.js 15 через App Router.
Рекомендуемая структура:
app/
├── page.tsx # Server Component, metadata, JSON-LD
├── components/
│ ├── hero-static.tsx # Server — текст, семантика
│ ├── hero-animation.tsx # 'use client' + dynamic(..., { ssr: false })
│ ├── faq-accordion.tsx # shadcn/ui Accordion, без framer-motion
│ ├── pricing-tabs.tsx # Tabs из shadcn/ui
│ └── scroll-reveal.tsx # lazy motion для блока «ниже fold»
Lazy load framer-motion для элементов ниже первого экрана:
'use client';
import dynamic from 'next/dynamic';
const MotionBenefits = dynamic(
() => import('./benefits-stagger').then((m) => m.BenefitsStagger),
{ ssr: false, loading: () => <BenefitsStatic /> }
);
ssr: false означает: motion-код не участвует в гидратации above-the-fold. Пользователь видит статический fallback мгновенно; анимация подгружается после requestIdleCallback или когда секция попадает во viewport через IntersectionObserver.
Для hero-анимации, которая всё же нужна above-the-fold, используйте CSS @keyframes в Server Component — 0 KB JS, LCP и INP не страдают:
@keyframes fade-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.hero-title {
animation: fade-up 0.6s ease-out both;
}
Framer-motion оставьте для micro-interactions: hover-scale на карточках, spring на модальном окне записи — там, где CSS неудобен, а элемент не участвует в первом критическом клике.
next/font и next/image — обязательные соседи: они не влияют на INP напрямую, но разгружают main thread при загрузке, сокращая input delay на ранних взаимодействиях.
Практические паттерны: useReducedMotion и приоритет потока
useReducedMotion — не опция, а требование
'use client';
import { motion, useReducedMotion } from 'framer-motion';
export function BenefitsStagger({ items }: { items: string[] }) {
const prefersReduced = useReducedMotion();
if (prefersReduced) {
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
return (
<motion.ul initial="hidden" animate="visible" variants={container}>
{items.map((item) => (
<motion.li key={item} variants={itemVariant}>
{item}
</motion.li>
))}
</motion.ul>
);
}
При prefers-reduced-motion: reduce framer-motion не загружается в критический путь — INP для части аудитории становится irrelevant, но accessibility и SEO (семантика <ul>/<li>) сохраняются.
Не анимируйте интерактивные элементы в первом viewport
Кнопка CTA, поле email, чекбокс согласия — никогда не оборачивайте в motion.button с whileTap. Используйте shadcn/ui <Button> с CSS :active или transition-transform. React обрабатывает один listener вместо gesture pipeline framer-motion.
startTransition для тяжёлых обновлений состояния
Если клик по табу тарифов пересчитывает большой JSX:
'use client';
import { startTransition, useState } from 'react';
export function PricingTabs() {
const [plan, setPlan] = useState('basic');
const handleSelect = (id: string) => {
startTransition(() => setPlan(id));
};
return (/* Tabs из shadcn/ui */);
}
startTransition помечает обновление как некритичное — браузер успевает отрисовать feedback на клик до тяжёлого re-render.
Резервирование высоты против CLS
Layout-анимации FAQ без фиксированной высоты вызывают CLS. Задайте min-height контейнеру или используйте overflow: hidden + CSS grid trick:
.accordion-content {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.25s ease;
}
.accordion-content[data-open] {
grid-template-rows: 1fr;
}
Замеры: Vercel Speed Insights и lab vs field
Без полевых данных оптимизация INP — guesswork. Подключите @vercel/speed-insights в layout.tsx:
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ru">
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}
После деплоя на Vercel в dashboard появятся реальные INP по URL, устройствам и географии. Сравнивайте до/после выноса framer-motion за dynamic import.
Lab-замеры (Lighthouse, WebPageTest) занижают INP: они эмулируют одно взаимодействие на чистой странице. Для лендинга важнее CrUX и Search Console → Core Web Vitals → «INP проблемы» по группам URL.
Чеклист перед релизом:
- Lighthouse mobile → Performance ≥ 90, INP (в разделе Insights) ≤ 200 ms в lab.
- WebPageTest → First Interactive Click с throttling 4G.
- Chrome DevTools → Performance → записать клик по CTA; main thread не забит дольше 50 ms непрерывно.
npm run build && npm run start— проверить, что production bundle не тянет motion в initial chunk (@next/bundle-analyzer).
Для edge deploy на Vercel статическая оболочка лендинга отдаётся из ближайшего PoP; client chunks кешируются с immutable hash — повторные визиты улучшают INP за счёт мгновенного JS из disk cache.
Нужна помощь? Telegram → или vic.kell@ya.ru
FAQ
Можно ли полностью отказаться от framer-motion на SEO-лендинге?
Да, и для большинства лендингов это лучший выбор. CSS animations + @view-transition (где поддерживается) покрывают 80% кейсов. Framer-motion оправдан для сложных gesture-интерфейсов, drag-and-drop конфигураторов, hero с path morphing — не для fade-in заголовка.
INP влияет на ранжирование в Google?
Напрямую как отдельный фактор — в рамках Page Experience и общей оценки качества. Google не публикует точный вес INP, но страницы с «Good» Core Web Vitals стабильно outperform конкурентов с «Poor» на коммерческих запросах. Плюс низкий INP повышает конверсию — косвенный SEO-сигнал через поведенческие факторы.
Почему INP плохой только на мобильных в CrUX?
Мобильные устройства слабее, throttling агрессивнее. Desktop INP часто «Good», mobile — «Poor». Оптимизируйте под mobile-first: меньше client JS, lazy motion, startTransition.
shadcn/ui ухудшает INP?
Сам по себе — минимально: Radix primitives лёгкие. Проблема возникает, когда shadcn-компоненты оборачивают в framer-motion или импортируют всю библиотеку icons целиком. Используйте tree-shaking, импортируйте иконки по одной из lucide-react.
Как связаны Partial Prerendering и INP?
PPR (Next.js 15) ускоряет отдачу HTML и улучшает LCP/TTFB, но не отменяет client-side JS для интерактива. INP зависит от того, сколько JS выполняется до и во время клика. PPR + минимальный client island — комбинация для идеального лендинга.
Стоит ли использовать will-change: transform на motion-элементах?
Точечно — да, для hero-декора above-the-fold. Массово — нет: каждый will-change резервирует compositor layer и ест память на мобильных. Предпочитайте transform и opacity — они не триггерят layout.
Заключение
INP — метрика, которую нельзя «отдать на потом» после запуска лендинга. Framer-motion делает страницу живой, но без дисциплины превращает первый клик в ожидание. На Next.js 15 App Router работает простая формула: Server Components для контента и SEO, shadcn/ui для форм и табов, framer-motion — lazy, below-the-fold, с useReducedMotion, CSS — для hero above-the-fold.
Измеряйте в Vercel Speed Insights и Search Console, а не только в Lighthouse. Лендинг с INP ≤ 200 ms конвертирует лучше и не теряет преимущество Page Experience в конкурентной выдаче 2026 года.