INP на лендинге Next.js 15: framer-motion без просадки — KEL IT
Сайты 9 мин чтения

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,1Layout-анимации без резервирования высоты

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:

  1. Статический HTML hero отдаётся из Server Component — заголовок, текст, CTA видны роботу и пользователю сразу.
  2. Анимация — тонкая client-обёртка вокруг декоративного слоя, не вокруг интерактивных элементов.
  3. Интерактив (кнопки, формы 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.

Чеклист перед релизом:

  1. Lighthouse mobile → Performance ≥ 90, INP (в разделе Insights) ≤ 200 ms в lab.
  2. WebPageTest → First Interactive Click с throttling 4G.
  3. Chrome DevTools → Performance → записать клик по CTA; main thread не забит дольше 50 ms непрерывно.
  4. 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 года.

KEL IT

Нужна разработка под ключ?

Я занимаюсь такими проектами профессионально. Telegram-боты, Mini Apps, сайты, мобильные и десктопные приложения. Расскажите о задаче — разберём и предложим решение.