INP on Next.js 15 Landing Pages: framer-motion Without Killing Core Web Vitals
Since 2024, Interaction to Next Paint (INP) has been a Core Web Vital — and for landing pages, it matters more than most teams realize. The first tap on “Book a demo,” the FAQ accordion, the pricing tab switch: each interaction must respond within 200 ms or Google flags the URL in Search Console and conversion rates drop.
The conflict is familiar. Beautiful landings rely on framer-motion — hero fades, staggered benefit cards, layout transitions. Every motion component ships JavaScript to the client. Mark the entire hero 'use client' and React hydrates a heavy bundle before clicks feel instant. INP spikes.
This guide covers a production pattern: Next.js 15 App Router, shadcn/ui for interactive UI, framer-motion only where it earns its bytes, and real-world measurement via Vercel Speed Insights. Target: INP ≤ 200 ms on mid-range mobile hardware.
Why INP Replaced FID — and What It Means for Landings
First Input Delay only measured delay until the browser started handling the first event. INP tracks all interactions during a session and reports a high percentile (roughly the 98th). A fast first click means nothing if the second — opening an FAQ item — stalls at 400 ms.
| Metric | Measures | ”Good” threshold | Typical landing pitfall |
|---|---|---|---|
| LCP | Main content paint speed | ≤ 2.5 s | Hero image without priority |
| INP | Interaction responsiveness | ≤ 200 ms | Blocking JS from animation libs |
| CLS | Layout stability | ≤ 0.1 | Animated height without reservation |
INP breaks down into input delay (main-thread queue), processing time (React handlers), and presentation delay (paint). Framer-motion hits all three: listeners, per-frame spring math, layout reflows.
Search Console surfaces INP in the Core Web Vitals report. URLs marked “Needs improvement” or “Poor” lose Page Experience benefits. On competitive queries, 100 ms of INP often correlates with rank and CTR differences you can actually see in analytics.
How framer-motion Hurts INP
Framer-motion bundles motion components, gesture engines, and layout projection. The classic mistake: wrapping the whole hero in a client component for one title animation:
// ❌ Bad: entire hero is client-side
'use client';
import { motion } from 'framer-motion';
export function Hero() {
return (
<motion.section initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<h1>Custom web development</h1>
<p>Shipped in 14 days. Green Core Web Vitals.</p>
<Button>Get in touch</Button>
</motion.section>
);
}
Until the browser downloads and executes ~30–45 KB gzip of framer-motion (plus React client runtime), the button looks ready but doesn’t respond. Input delay grows.
The Next.js 15 App Router fix:
- Static HTML from Server Components — headline, copy, CTA visible to crawlers immediately.
- Animation as a thin client wrapper around decorative layers, not interactive controls.
- Interactivity (buttons, shadcn/ui forms) in separate, minimal client components.
// ✅ HeroStatic — Server Component
import { HeroAnimation } from './hero-animation';
import { BookingButton } from './booking-button';
export function HeroStatic() {
return (
<section className="relative">
<HeroAnimation />
<h1>Custom web development</h1>
<p>Shipped in 14 days. Green Core Web Vitals.</p>
<BookingButton />
</section>
);
}
Layout animations (layout, layoutId) are INP killers on FAQ and tabs. Each toggle recalculates geometry for all motion descendants. For SEO FAQ blocks, prefer CSS grid-template-rows: 0fr → 1fr or Radix Accordion from shadcn/ui without layout projection.
If you need help building this — message on Telegram.
Client Boundaries and Lazy Loading
Same idea as Astro Islands, implemented through App Router boundaries:
app/
├── page.tsx # Server — metadata, JSON-LD
├── components/
│ ├── hero-static.tsx # Server — semantics
│ ├── hero-animation.tsx # 'use client' + dynamic(..., { ssr: false })
│ ├── faq-accordion.tsx # shadcn/ui, no framer-motion
│ └── scroll-reveal.tsx # lazy motion below the fold
Lazy-load framer-motion for below-the-fold sections:
'use client';
import dynamic from 'next/dynamic';
const MotionBenefits = dynamic(
() => import('./benefits-stagger').then((m) => m.BenefitsStagger),
{ ssr: false, loading: () => <BenefitsStatic /> }
);
With ssr: false, motion code skips above-the-fold hydration. Users see a static fallback instantly; animation loads after idle time or when IntersectionObserver fires.
For above-the-fold motion you truly need, CSS @keyframes in a Server Component costs 0 KB JS:
@keyframes fade-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
Reserve framer-motion for micro-interactions — hover scale on cards, spring modals — elements that aren’t involved in the first critical click.
Patterns That Keep INP Green
useReducedMotion
const prefersReduced = useReducedMotion();
if (prefersReduced) return <StaticList items={items} />;
return <motion.ul /* stagger variants */ />;
When users prefer reduced motion, skip the library entirely on that code path.
Never wrap first-viewport CTAs in motion
Use shadcn/ui <Button> with CSS :active feedback. One React listener beats framer-motion’s gesture pipeline.
startTransition for heavy state updates
Tab switches that re-render large trees should wrap setState in startTransition so the browser paints click feedback first.
Reserve height to avoid CLS
FAQ animations without fixed height cause layout shift. Use CSS grid row transitions or explicit min-height on containers.
Measuring: Vercel Speed Insights vs Lab Tools
Add @vercel/speed-insights to layout.tsx and deploy on Vercel. The dashboard shows field INP by URL, device, and region — compare before/after moving framer-motion behind dynamic imports.
Lab tools (Lighthouse, WebPageTest) underreport INP: one synthetic interaction on a cold page. Trust CrUX and Search Console field data for production decisions.
Pre-release checklist:
- Lighthouse mobile — INP insight ≤ 200 ms in lab.
- WebPageTest — first interactive click on 4G throttling.
- Chrome Performance panel — record a CTA click; no 50 ms+ main-thread blocks.
@next/bundle-analyzer— confirm motion isn’t in the initial chunk.
Edge deployment on Vercel serves the static shell from the nearest PoP; hashed client chunks cache immutably — repeat visits improve INP via disk cache.
Need help? Telegram → or vic.kell@ya.ru
FAQ
Should I drop framer-motion entirely on SEO landings?
Often yes. CSS animations cover most cases. Framer-motion earns its place for complex gestures, drag-and-drop configurators, path morphing — not for a headline fade-in.
Does INP directly affect Google rankings?
As part of Page Experience and overall quality signals — yes, indirectly. Google doesn’t publish INP weight, but “Good” Core Web Vitals URLs consistently outperform “Poor” ones on commercial queries. Lower INP also lifts conversion — a behavioral signal search engines notice over time.
Why is mobile INP worse in CrUX than desktop?
Weaker CPUs, aggressive throttling. Optimize mobile-first: less client JS, lazy motion, startTransition.
Does shadcn/ui hurt INP?
Minimally — Radix is lean. Problems appear when wrapping shadcn components in framer-motion or importing entire icon libraries. Tree-shake and import Lucide icons individually.
How do PPR and INP relate?
Partial Prerendering improves HTML delivery and LCP, but doesn’t remove client JS for interactivity. INP depends on JS executed before and during clicks. PPR plus minimal client islands is the winning combo.
Conclusion
INP isn’t a post-launch afterthought. Framer-motion makes landings feel alive, but undisciplined use turns the first click into a wait. On Next.js 15: Server Components for content and SEO, shadcn/ui for forms and tabs, framer-motion lazy and below-the-fold with useReducedMotion, CSS for above-the-fold hero motion.
Measure with Vercel Speed Insights and Search Console, not Lighthouse alone. A landing with INP ≤ 200 ms converts better and holds its Page Experience edge in 2026 search results.