How to Fix LCP on Mobile (A Real Playbook From a 7.3s Disaster)
Last month I opened Lighthouse on a client site and watched my mobile performance score drop to 61. Largest Contentful Paint came in at 7.3 seconds. On a real 4G connection, the hero image took so long to render that users were scrolling away before they even saw what the business did.
If you've never seen LCP fall off a cliff on mobile, it's a specific kind of pain. Desktop Lighthouse looks fine. Your home network is fast. You open it on your phone on cellular and the page is an empty white box for seven seconds. Then the hero image pops in. Then the fonts swap. Then some animation runs. By the time the page is usable, your visitor is gone.
Here is the actual diagnostic I ran, the fixes I shipped, and the final numbers. LCP dropped from 7.3 seconds to 1.8 seconds. Lighthouse mobile went from 61 to 97. If you are fighting the same fight, this playbook should get you there.
What LCP Actually Measures
Largest Contentful Paint is the time from when the user requests the page to when the largest visible element finishes rendering. On most marketing sites, that's the hero image or the H1 headline. On e-commerce, it's usually the product image. On blog posts, it's the featured image or the first paragraph.
Google's official thresholds:
- Good: under 2.5 seconds - Needs improvement: 2.5 to 4.0 seconds - Poor: over 4.0 seconds
Anything over 4.0 seconds is actively hurting your rankings. It's a Core Web Vital, and Core Web Vitals are a confirmed ranking factor for mobile search.
The Diagnostic: Find the Actual LCP Element
Open Chrome DevTools, go to the Performance tab, enable mobile throttling (Fast 3G or Slow 4G, not "No throttling"), and record a page load. After the recording, look at the Timings row. You'll see a green LCP marker. Hover over it and DevTools tells you exactly which element was the LCP.
In my case, it was the hero image. A 1.4 MB PNG loaded at full width on mobile. That was the smoking gun.
But there's more. When you look at the Network waterfall, you want to understand WHY that image took 7.3 seconds. In my case, four separate things were blocking it.
Problem 1: The Hero Image Wasn't Prioritized
Next.js `<Image>` component is great, but by default every image is lazy-loaded. That means the hero image, which is immediately visible, was waiting in line behind every other resource on the page.
The fix: Mark the hero image as priority.
```tsx import Image from 'next/image';
<Image src="/hero.webp" alt="Main hero image" width={1200} height={600} priority sizes="(max-width: 768px) 100vw, 1200px" /> ```
The `priority` prop tells Next.js to preload the image with `<link rel="preload">` in the document head. It gets fetched immediately instead of waiting for lazy-load. This alone shaved 2.1 seconds off my LCP.
The `sizes` attribute is equally important. Without it, browsers fetch the largest image in the srcset regardless of screen size. On mobile, that means downloading a 1200px image to display at 375px. With proper sizes, the browser picks the right one.
Problem 2: Fonts Were Blocking the First Paint
The site was using a custom Google Font loaded via `<link>` tag in the layout. Default font-display behavior is "auto", which in most browsers means "block for up to 3 seconds while the font loads." During those 3 seconds, text isn't rendered, and the LCP element can't paint if it contains text.
The fix: Use `font-display: swap` and preload the font.
In Next.js 14+ with the App Router, use `next/font`. It handles everything automatically:
```tsx import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap', preload: true, });
export default function RootLayout({ children }) { return ( <html lang="en" className={inter.className}> <body>{children}</body> </html> ); } ```
`display: swap` tells the browser to render text immediately with a fallback font, then swap to the custom font once it loads. Users see text faster. LCP fires faster. No more 3-second blank screen.
If you're not on Next.js, add this to your CSS manually:
```css @font-face { font-family: 'Inter'; src: url('/fonts/inter.woff2') format('woff2'); font-display: swap; } ```
And preload it in your head:
```html <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> ```
Font fixes saved me another 1.4 seconds.
Problem 3: Render-Blocking JavaScript
The site loaded three third-party scripts in the head: Google Tag Manager, Hotjar, and Intercom. Total weight: 340 KB of JavaScript that had to parse and execute before the browser could render anything.
The fix: Defer all non-critical scripts.
In Next.js, use the `<Script>` component with the `afterInteractive` or `lazyOnload` strategy:
```tsx import Script from 'next/script';
// Analytics: load after page is interactive <Script src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX" strategy="afterInteractive" />
// Chat widget: load when browser is idle <Script src="https://widget.intercom.io/widget/abc123" strategy="lazyOnload" /> ```
`afterInteractive` loads the script after the page is interactive (after hydration). `lazyOnload` waits until the browser is idle, which is perfect for chat widgets and non-critical analytics. Neither blocks the initial render.
If you're on plain HTML, add `defer` or `async` to every `<script>` tag in the head. Better yet, move them to the end of `<body>` so they don't block rendering at all.
This saved another 0.9 seconds.
Problem 4: Framer Motion on Hydration
This is the sneaky one. The hero section used Framer Motion animations to fade in on mount. The animations fired during hydration, which meant:
1. The server rendered the hero image (visible in HTML) 2. React hydrated 3. Framer Motion's initial state set the hero opacity to 0 4. The animation ran and faded it back to 1 5. Only AFTER the animation finished did the browser consider the image "painted"
LCP was measuring the END of the animation, not the initial render. Fixing this was the biggest single win.
The fix: Don't animate the LCP element on initial load.
```tsx // Before (bad) <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}> <Image src="/hero.webp" priority ... /> </motion.div>
// After (good) <div> <Image src="/hero.webp" priority ... /> </div> ```
Keep animations for below-the-fold content where they don't affect LCP. Or use CSS animations with `animation-delay` so the element paints first, then animates after.
Alternatively, if you really need the animation, use a `whileInView` trigger instead of `animate` so it only fires when the element scrolls into view:
```tsx <motion.div initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }} > ```
This fix alone dropped LCP by another 2.1 seconds. It also fixed a CLS issue I didn't know I had.
The Final Numbers
Before: LCP 7.3s, Performance 61, FCP 3.1s After: LCP 1.8s, Performance 97, FCP 0.9s
Four fixes. Zero changes to the visual design. The site looks identical. It just loads 4x faster on mobile.
The Pattern
Every LCP problem I've debugged on mobile falls into one of these four buckets:
1. The LCP element isn't prioritized (missing `priority`, wrong `sizes`, no preload) 2. Fonts are blocking the first paint (no `font-display: swap`, no preload) 3. Third-party scripts are blocking render (sync scripts in head, no defer) 4. JavaScript animations are delaying paint (Framer Motion, GSAP, or CSS transitions on the LCP element)
If your mobile LCP is over 2.5 seconds, it's almost certainly one or more of these. The fixes are usually 20 to 50 lines of code total.
Let the Tool Find It For You
If you want to skip the manual diagnostic, our DeepAudit AI tool runs your site through a headless browser, measures LCP on a simulated mobile connection, and tells you exactly which element is the bottleneck. It flags missing priority hints, render-blocking scripts, and font loading issues in one report.
For a full performance overhaul beyond just LCP, check out our website speed optimization service. We do this for clients every week. LCP fixes are usually the single highest-ROI work we ship: better rankings, lower bounce rates, higher conversions, all from a few dozen lines of code.
If mobile performance is actively hurting your business, book a call and we'll walk through your site together. Most of the time the fixes are shorter than this blog post.
Related services
Ready to build a website that performs?
Let us audit your current site, identify the biggest opportunities, and build a plan to grow your traffic and leads.