Why Your AI-Built Site Fails Lighthouse
The v0 hero you shipped can land in the red on mobile. Here's the exact map from each slop pattern (framer-motion bundles, JS-gated reveals, a multi-megabyte stock hero) to the audit it tanks, and the fix.
On this page
Picture a landing page you have seen a hundred times: clean, dark, a bg-zinc-950 hero with a gradient headline, three feature cards that fade up on scroll. Built in v0, lightly edited in Cursor, shipped to Vercel. It looks fine. Now run Lighthouse on it, mobile, with the default throttling. The numbers below are an illustration of what a page built this way can score, not a measurement of a real site:
Performance 41
LCP 5.8 s
TBT 610 ms
CLS 0.31
First Load JS 412 KBForty-one, in this illustration. A site that "looks fine" can be in the red on every Core Web Vital. None of these numbers is random. Each one traces back to a specific, recognizable slop pattern: the same patterns that make these sites look identical in the first place. The visual sameness and the performance failure come from the same generated defaults. Here they are, one failing audit at a time.
CLS 0.31: the scroll-reveal is eating your layout
Cumulative Layout Shift measures how much stuff jumps around as the page loads. Google's "good" threshold is 0.1. "Poor" starts at 0.25. A page like this one can pass 0.25 without trying, and you can guess where it comes from before you open the source, because it's the single most common AI-generated motion pattern: the fade-in-up scroll reveal.
Here's the typical shape of the generated code:
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<FeatureCard />
</motion.div>The problem isn't the animation itself: it's opacity: 0 combined with content that hasn't reserved its space. Worse is the common variant where the whole section is display: none or height-collapsed until it enters the viewport. When the IntersectionObserver fires, the card pops into existence and everything below it lurches down. That lurch is your CLS. Every card on the page that animates in on scroll is a layout-shift event waiting to happen, and AI scaffolds put this on everything: hero, features, testimonials, footer CTA.
There is a whole piece on why this exact pattern is a motion-slop tell; the performance cost is the part nobody mentions when they paste it in.
The fix is not "remove the animation." It's: reserve the space, then animate inside it. The element must occupy its final dimensions from first paint. Animate opacity and transform only: both are composited properties that never trigger layout:
/* The wrapper holds its size. Only the visual transforms. */
<div className="min-h-[var(--card-h)]">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
style={{ willChange: "transform, opacity" }}
>
<FeatureCard />
</motion.div>
</div>transform: translateY() does not affect surrounding layout: the browser composites it on the GPU and nothing reflows. So the card can slide up 24px visually while its layout box never moves. CLS contribution: zero. Even simpler, drop JS entirely and use a CSS @keyframes with animation-timeline: view() where browser support allows, so there's no observer, no hydration dependency, no shift.
LCP 5.8s: the hero image nobody compressed
Largest Contentful Paint is when the biggest above-the-fold element finishes rendering. Good is under 2.5s. In the illustration it takes 5.8s, and the usual culprit is a single file: a hero.jpg of a few megabytes, 4000×2667, served as a raw JPEG straight from a stock-photo download.
This is the most boring failure in the catalog and the most common. The AI builder generates <img src="/hero.jpg" /> or a CSS background-image, you drop in a stock photo, and nobody ever runs it through anything. The browser downloads two-and-a-half megabytes over a throttled mobile connection before it can paint the hero. On a 1.6 Mbps Slow 4G profile, that download alone is roughly 12 seconds of theoretical transfer; even with real-world ramping you're looking at multiple seconds of dead screen.
Three things are wrong and each one independently tanks LCP:
- Format. It's a JPEG. Google measures WebP lossy images at 25 to 34% smaller than comparable JPEGs (Google), and AVIF usually goes further.
- Dimensions. It's 4000px wide rendering into a container that's never more than 1200px. You're shipping 11x the pixels you display.
- Priority. There's no
fetchpriority="high", no preload. The browser discovers the hero late, after it's parsed the CSS that references it.
The fix, using Next.js <Image> which handles most of this if you let it:
import Image from "next/image";
import hero from "@/public/hero.jpg"; // local import = build-time optimization
<Image
src={hero}
alt="..."
priority // preloads + fetchpriority=high
sizes="100vw"
placeholder="blur" // no CLS while it loads
quality={70}
/>If you're not on Next, the raw-HTML equivalent does the same work explicitly:
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">
<img
src="/hero.avif"
width="1200" height="800"
fetchpriority="high"
decoding="async"
alt="...">Compress that JPEG to a properly-sized AVIF and it typically shrinks to a small fraction of its weight. LCP falls with it. One file, and you've done most of the work on the single hardest Core Web Vital.
Note the width and height attributes: those also feed back into CLS, because an <img> without intrinsic dimensions reserves zero space until it loads, then shoves the page down when it arrives. AI builders omit them constantly.
TBT 610ms: framer-motion is 34 KB you didn't need
Total Blocking Time is how long the main thread is frozen during load: unable to respond to taps, scrolls, anything. Good is under 200ms. A page like this one easily sits several times over, and a big contributor is JavaScript that exists purely to do animations CSS could do for free.
Crack open the bundle. framer-motion (now motion) is roughly 34 KB gzipped for the core, more once you pull in layout animations and gestures. On a page like the one above it is used for exactly one thing: the fade-in-up reveals. Thirty-four kilobytes of parse-compile-execute, blocking the main thread, to fade some cards in. The same effect in CSS is zero bytes of JavaScript:
@media (prefers-reduced-motion: no-preference) {
.reveal {
animation: fade-up 0.6s ease-out both;
animation-timeline: view();
animation-range: entry 0% cover 30%;
}
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}No library. No hydration. No main-thread cost. And for the browsers that don't support scroll-driven CSS animations yet, the content just shows, which is the correct fallback anyway.
This is the broader pattern with AI-built React sites: they reach for a heavy dependency for every small interaction. A counter that animates up? react-countup. A carousel? embla plus framer-motion. A tooltip? An entire Radix import. Each one is fine in isolation; stacked, they produce a First Load JS of 412 KB where a hand-built equivalent ships 80-120 KB. The TBT is the sum of parsing and executing all of it before the page is interactive.
The audit to watch is "Reduce unused JavaScript" and "Avoid serving legacy JavaScript." Run npx @next/bundle-analyzer or vite-bundle-visualizer and look at the treemap. On AI-built sites, the three fattest blocks are usually: the animation library, an icon library imported wholesale (import * as Icons pulls in all of lucide), and a date/charting library used on one component. Tree-shake the icons (import { ArrowRight } from "lucide-react", never the barrel), lazy-load anything below the fold, and replace framer-motion's reveals with CSS.
First Load JS 412 KB: the hydration tax
There's a quieter failure underneath all of this. The page is mostly static: a hero, some cards, a pricing table, a footer. None of it needs client-side JavaScript to render. But because the AI scaffold made everything a client component (every file gets "use client" slapped on top by default in a lot of generated output), the entire thing ships its markup and a JS payload to re-render that same markup on the client. You pay for the HTML twice.
In the Next App Router, the fix is to delete "use client" from everything that doesn't have an event handler or a hook. A <FeatureCard> that's just markup is a Server Component: it ships as HTML, zero JS. Push the "use client" boundary down to the smallest leaf that actually needs interactivity: the one <button> with an onClick, not the section that contains it.
// page.tsx - Server Component, no "use client"
export default function Page() {
return (
<>
<Hero /> {/* static, 0 KB JS */}
<Features /> {/* static, 0 KB JS */}
<SignupButton /> {/* this one is "use client" */}
</>
);
}That single discipline (interactivity at the leaves, static everything else) is what separates a 120 KB first load from a 412 KB one. The AI default does the opposite because it's safer for the generator to mark everything client-side; it just costs you the whole performance budget.
Why this is also an SEO problem
These aren't vanity numbers. Core Web Vitals are a confirmed ranking signal, and Google's documentation is explicit that page experience compounds with content quality. A site that's both thin AI content and slow gets hit from both directions: the helpful-content systems discount the text, and the CWV signals discount the page. A score in the red isn't just a developer-experience embarrassment; on a competitive query, page experience is one of the signals that can separate you from a faster competitor.
The 15-minute audit
You don't need a methodology. Open the deployed site, DevTools, Lighthouse tab, mobile, analyze. Then walk the failures top to bottom:
- CLS over 0.1 → find the scroll reveals and the dimensionless images. Reserve space, add
width/height, animate onlytransform/opacity. - LCP over 2.5s → it's usually the hero image. AVIF, correct dimensions,
priority/preload. - TBT over 200ms → bundle analyzer. Kill framer-motion if it's only doing reveals. Tree-shake icons. Lazy-load below-fold JS.
- First Load JS over ~150 KB → hunt
"use client". Push the boundary to the leaves.
Every one of these maps to a generated default, not a design decision you made. That's the tell. The site is slow for the same reason it looks like every other AI site: nobody made a choice, the model did, and the model optimizes for output that compiles, not output that's fast.
Run Lighthouse before you ship. If it's under 90 on mobile, the patterns above are almost certainly why, and the fixes above are almost certainly enough. The whole audit is fifteen minutes. The oversized hero image alone is often the biggest single win.
Revision note
Updated 18 October 2026. An earlier version presented the Lighthouse run as a real page that "came across my desk last week", with a score of 41 and precise metrics, and gave unsourced compression and frequency figures. The run is now labeled as an illustration, the WebP figure cites Google, and the other figures were removed.
Scan a live page
Eight AI defaults, the evidence for each, and a fix kit. Free, no account.