Core Web Vitals for Lovable and Framer Sites: Why They Fail and How to Fix Them

TL;DR — Quick answer

Typical scores before fixing: Lovable 20–40 mobile, Framer 65–78 mobile. After fixes: both reach 85–95. The highest-impact actions:

  1. Convert images to WebP + add width/height — fixes LCP and CLS simultaneously
  2. Enable React code splitting (Lovable) — cuts JS bundle size 40–60%
  3. Preload the hero image — immediate LCP improvement on first load
  4. Reserve space for dynamic content — eliminates layout shift
  5. Defer non-critical third-party scripts — removes render blocking
  6. Self-host Google Fonts or use font-display: swap — eliminates invisible text flash

Lovable requires code-level fixes. Framer fixes are mostly in the settings panel and custom code injection. Both are achievable without a full rebuild.

Open PageSpeed Insights and test your Lovable site. The score that comes back — typically somewhere between 18 and 42 on mobile — is not random. It is the predictable outcome of how Lovable builds React applications by default.

Now test a Framer site. The score is better — usually 65 to 78 — because Framer uses server-side rendering. But it still fails the Core Web Vitals thresholds that Google uses as a ranking signal.

Both platforms ship code that performs below Google’s standards. Neither provides the optimization tooling to fix it without manual intervention. And most builders — focused on features and design — never run PageSpeed Insights at all.

This guide covers exactly what causes the poor scores on each platform and exactly how to fix them. Lovable and Bolt.new share the same root causes (both are React SPAs). Framer has a different, smaller set of problems. I’ll cover both.

Understanding Core Web Vitals: What Google Actually Measures

Google measures three Core Web Vitals, each targeting a different dimension of user experience:

LCP — Largest Contentful Paint
The time from page navigation to when the largest visible element (usually the hero image or main headline) is fully rendered. Target: under 2.5 seconds. Most Lovable sites: 4–8 seconds.

CLS — Cumulative Layout Shift
A measure of how much the page layout shifts unexpectedly during loading — content jumping around as images load, fonts swap, or dynamic content appears. Target: under 0.1. Many vibe-coded sites: 0.15–0.45.

INP — Interaction to Next Paint
The time from a user interaction (click, tap, keystroke) to the next visual response. Replaced FID in March 2024. Target: under 200 milliseconds. Heavy React bundles can push this above 400ms.

Google measures these using real user data from Chrome browsers (the Chrome User Experience Report, or CrUX). The rankings impact comes from field data — actual user experiences, not just lab tests. This means you need sustained real-user performance improvement, not just a good PageSpeed lab score.

That said, lab scores (PageSpeed Insights) are a reliable proxy and the fastest way to diagnose and verify fixes.

Lovable and Bolt.new: Why Scores Are 20–40 on Mobile

Lovable and Bolt.new generate React single-page applications. The root architecture creates three performance problems that compound each other:

Problem 1: Monolithic JavaScript bundle

Every React app has a JavaScript bundle — the compiled code that runs the application. By default, Lovable and Bolt.new compile everything into a single large file, typically 400KB to 1.5MB before gzip. Before this file downloads, parses, and executes, the browser shows nothing. This is the primary driver of high LCP.

On a fast desktop connection this might take 1–2 seconds. On a typical mobile connection in a developing market, it takes 5–8 seconds. Google tests on simulated mobile — which is why mobile scores are always dramatically lower than desktop.

Problem 2: Unoptimized images

Lovable and Bolt.new do not process images. Whatever you upload or reference — full-resolution JPEG, PNG with transparency, images far larger than their display size — gets served as-is. A 3MB hero image with a 4000px width served to a mobile device displaying it at 390px is 10× the necessary file size.

Problem 3: No resource prioritization

The browser doesn’t know which resources are most important unless you tell it. Without preloading hints and lazy loading, the browser fetches everything at equal priority — including below-the-fold images, third-party analytics scripts, and fonts that aren’t needed until scroll.

These three problems work together: the browser is downloading a huge JS bundle, a giant image, and a dozen third-party scripts simultaneously, with no guidance on what to prioritize.

Lovable and Bolt.new: The Fix Sequence

Apply these in order — each fix builds on the previous one.

Fix 1: Convert All Images to WebP and Add Explicit Dimensions

This single change typically improves LCP by 1–3 seconds and eliminates most CLS.

WebP conversion: WebP images are 25–35% smaller than JPEG at equivalent quality, and 60–80% smaller than PNG. Convert every image before uploading.

Free tools: squoosh.app (browser-based, excellent quality control), ImageOptim (Mac), or the sharp Node.js library for bulk conversion.

Target sizes:

  • Hero/full-width images: under 200KB, max 1440px wide
  • Card/thumbnail images: under 50KB, sized to display dimensions
  • Icons and UI elements: SVG preferred, or PNG under 10KB

Explicit dimensions: Every <img> tag needs width and height attributes matching the image’s natural dimensions:

// Before — causes CLS as the browser doesn't know how much space to reserve
<img src="/hero.webp" alt="Hero image" />

// After — browser reserves exact space before image loads, preventing layout shift
<img
  src="/hero.webp"
  alt="Descriptive alt text including your primary keyword naturally"
  width="1440"
  height="810"
  loading="lazy"
  decoding="async"
/>

Add loading="eager" (or omit loading entirely) for the hero image — it is the LCP element and should not be lazy loaded.

Add loading="lazy" to all images below the first viewport.

Fix 2: Enable React Code Splitting

Code splitting breaks your monolithic JavaScript bundle into smaller chunks that load on demand. Instead of downloading all 1.2MB of JavaScript upfront, the browser downloads only the code needed for the current page.

In your Lovable or Bolt.new project, replace direct component imports with lazy imports:

// Before — everything loads upfront
import AboutPage from './pages/About';
import ServicesPage from './pages/Services';
import ContactPage from './pages/Contact';
import BlogPage from './pages/Blog';

// After — each page loads only when the user navigates to it
import { lazy, Suspense } from 'react';

const AboutPage    = lazy(() => import('./pages/About'));
const ServicesPage = lazy(() => import('./pages/Services'));
const ContactPage  = lazy(() => import('./pages/Contact'));
const BlogPage     = lazy(() => import('./pages/Blog'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div style={{minHeight:'100vh'}} />}>
        <Routes>
          <Route path="/about"    element={<AboutPage />} />
          <Route path="/services" element={<ServicesPage />} />
          <Route path="/contact"  element={<ContactPage />} />
          <Route path="/blog"     element={<BlogPage />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

The Suspense fallback needs a minHeight matching roughly the page height — this prevents CLS during the loading state.

Expected improvement: Reduces initial JavaScript bundle by 40–65% for most sites. This alone typically moves LCP from 5–7s to 2.5–4s.

Fix 3: Preload the Hero Image

The hero image is almost always the LCP element. By default, the browser discovers it only after parsing the HTML and rendering some CSS. A preload hint tells the browser to fetch it immediately alongside the HTML:

Add to your index.html <head>:

<link
  rel="preload"
  as="image"
  href="/your-hero-image.webp"
  fetchpriority="high"
/>

If your hero image is defined in CSS as a background image, use:

<link
  rel="preload"
  as="image"
  href="/your-hero-bg.webp"
  fetchpriority="high"
  imagesrcset="/hero-mobile.webp 768w, /hero-desktop.webp 1440w"
  imagesizes="100vw"
/>

Expected improvement: Typically reduces LCP by 0.5–1.5 seconds. Combined with WebP conversion, this frequently brings LCP under 2.5s on its own.

Fix 4: Self-Host Fonts or Use font-display: swap

Google Fonts loaded from external CDN add a network round-trip and block text rendering until the font loads. This shows as invisible text in the LCP measurement (text is the LCP element on many pages).

Option A: Add font-display: swap

In your CSS or Google Fonts URL, add &display=swap:

<!-- Before -->
<link href="https://fonts.googleapis.com/css2?family=DM+Sans" rel="stylesheet">

<!-- After -->
<link href="https://fonts.googleapis.com/css2?family=DM+Sans&display=swap" rel="stylesheet">

font-display: swap shows fallback system fonts immediately, then swaps to your custom font when it loads. This means text is visible immediately (reducing LCP) but causes a brief font swap (slight CLS trade-off — worth it).

Option B: Self-host fonts (best performance)

Download your fonts from google-webfonts-helper.herokuapp.com, add them to your project’s public/fonts/ directory, and serve them locally:

@font-face {
  font-family: 'DM Sans';
  src: url('/fonts/dm-sans.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

Then preload in <head>:

<link rel="preload" href="/fonts/dm-sans.woff2" as="font" type="font/woff2" crossorigin>

Self-hosting eliminates the Google Fonts DNS lookup, connection, and CDN latency. For most sites this saves 150–400ms on font loading.

Fix 5: Defer Non-Critical Third-Party Scripts

Third-party scripts — analytics, chat widgets, A/B testing tools, marketing pixels — loaded in <head> without defer or async block HTML parsing and delay everything else.

Audit every third-party script in your index.html:

<!-- Blocks rendering — bad -->
<script src="https://cdn.chatwidget.com/widget.js"></script>

<!-- Non-blocking — good -->
<script src="https://cdn.chatwidget.com/widget.js" defer></script>

<!-- For scripts that don't need DOM — even better -->
<script src="https://cdn.chatwidget.com/widget.js" async></script>

For chat widgets and non-essential tools, load them only after the user interacts with the page:

// In React — load chat widget on first user interaction
useEffect(() => {
  const loadChat = () => {
    const script = document.createElement('script');
    script.src = 'https://cdn.chatwidget.com/widget.js';
    document.head.appendChild(script);
    // Remove listeners after first load
    window.removeEventListener('mousemove', loadChat);
    window.removeEventListener('touchstart', loadChat);
  };
  window.addEventListener('mousemove', loadChat, { once: true });
  window.addEventListener('touchstart', loadChat, { once: true });
}, []);

This pattern loads the chat widget only after the user moves their mouse or touches the screen — meaning real users get the chat widget, but PageSpeed bots (which don’t interact) don’t penalize you for it.

Expected improvement: Removing render-blocking scripts typically improves both LCP and INP by 200–600ms.

Fix 6: Reserve Space for All Dynamic Content

CLS happens when content loads and pushes other content around. Common sources on vibe-coded sites:

Images without dimensions — fixed by Fix 1 above.

Dynamic content blocks — if your React components fetch data and render conditionally, the empty state and loaded state often have different heights.

Fix by defining minimum heights for loading states:

// Bad — causes layout shift when content loads
function PricingCard({ plan }) {
  if (!plan) return null;
  return <div>{plan.name}</div>;
}

// Good — reserves space during load
function PricingCard({ plan }) {
  if (!plan) return <div style={{ minHeight: '200px', background: '#f0f0f0', borderRadius: '12px' }} />;
  return <div>{plan.name}</div>;
}

Embeds and iframes — always wrap in an aspect-ratio container:

.video-wrapper {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
}
.video-wrapper iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

After all six Lovable/Bolt.new fixes, typical results:

  • LCP: from 5–8s to 1.5–2.5s
  • CLS: from 0.2–0.4 to 0.02–0.08
  • INP: from 250–400ms to 80–150ms
  • PageSpeed mobile: from 20–40 to 80–93

Framer: Why Scores Are 65–78 on Mobile

Framer’s server-side rendering means the browser receives real HTML immediately — eliminating the biggest Lovable/Bolt.new problem. But Framer has its own set of performance issues that prevent reaching 90+.

Problem 1: Large uncompressed images

Framer does not compress images by default. A full-resolution PNG uploaded by a designer and displayed as a hero image is the single most common cause of Framer’s LCP failures.

Problem 2: Framer animation runtime

Framer’s built-in animations and interactions are powered by Framer Motion, which adds JavaScript weight to every page even when only a few animations are used. Pages with many scroll-triggered animations can have significant INP delays.

Problem 3: Third-party scripts without deferral

Same as Lovable — analytics, chat widgets, and marketing tools added to the custom code head without defer attributes.

Problem 4: Web fonts from Google CDN

Same loading pattern as described above — fixable with font-display: swap or self-hosting.

Framer: The Fix Sequence

Framer fixes are simpler than Lovable fixes — most happen in the Framer settings panel, not in code.

Fix 1: Compress Every Image Before Uploading

The most impactful fix for Framer sites. Before uploading any image to Framer:

  1. Resize to the maximum display width (most desktop hero images display at max 1440px — uploading at 4000px wastes 87% of the file size)
  2. Convert to WebP using squoosh.app
  3. Target under 150KB for hero images, under 50KB for smaller images

After uploading, Framer serves images from its CDN, but the base file size determines how long the download takes. A 1.8MB PNG and a 140KB WebP of the same image display identically — the WebP just loads 12× faster.

Fix 2: Enable Lazy Loading on Below-Fold Images

In Framer, click any image → Properties panel → find Lazy Load toggle → enable it for all images that appear below the first viewport.

Do not enable lazy load on:

  • Your hero image (above fold, LCP element — needs to load immediately)
  • Any image visible without scrolling on a typical mobile screen

Fix 3: Defer Third-Party Scripts

In Framer → Site Settings → General → Custom Code → Head, find any third-party scripts and add defer:

<!-- Before -->
<script src="https://cdn.intercom.io/widget.js"></script>

<!-- After -->
<script src="https://cdn.intercom.io/widget.js" defer></script>

Move non-critical scripts from the Head section to Body End (also in Framer’s Custom Code settings). Scripts in body end load after the page content renders.

Fix 4: Reduce Animation Complexity

Heavy scroll-triggered animations increase INP. In Framer’s interactions panel:

  • Remove animations from above-the-fold elements (they delay LCP perception)
  • Reduce simultaneous animations — if every section has a scroll animation, simplify some to opacity-only transitions
  • Use will-change: transform sparingly — overuse creates additional composite layers that slow painting

Fix 5: Add Font Preload

In Framer → Site Settings → Custom Code → Head, add font preload hints for your primary font:

<link rel="preload" href="https://fonts.gstatic.com/s/spacegrotesk/v16/V8mDoQDjQSkFtoMM3T6r8E7mF71Q-gozuXTPTg.woff2" as="font" type="font/woff2" crossorigin>

You can find the exact woff2 URL for your Google Font by loading the page in Chrome → DevTools → Network tab → filter by “Font” → copy the request URL for your font file.

After all Framer fixes, typical results:

  • LCP: from 3–4.5s to 1.2–2s
  • CLS: from 0.05–0.15 to 0–0.04
  • INP: from 150–250ms to 60–120ms
  • PageSpeed mobile: from 65–78 to 88–96

Comparing Platforms: Core Web Vitals Starting Points

Understanding your starting position helps set expectations:

PlatformTypical Mobile LCPTypical Mobile CLSTypical PageSpeedRender Method
Lovable5–8s0.2–0.420–40Client-side React
Bolt.new4–7s0.15–0.3525–45Client-side React
Cursor4–8s0.1–0.320–50Client-side React
Framer2.5–4.5s0.05–0.1565–78Server-side
Webflow2–3.5s0.02–0.172–85Server-side
WordPress (optimized)1–2.5s0–0.0585–95Server-side

The render method (client-side vs server-side) is the single biggest factor. For React-based vibe-coding tools, adding prerendering or SSR is the highest-leverage single change — but also the most complex. The fixes in this guide target improvements achievable without switching rendering methods.

How to Measure Your Before and After

Always create a documented before/after comparison — both for your own tracking and as client proof if you’re doing this professionally.

Step 1: Run baseline test

  • Go to pagespeed.web.dev
  • Test your URL on mobile (the default)
  • Screenshot the full results page
  • Note: LCP time, CLS score, INP score, and the overall Performance score
  • Run the test 3 times and take the median — single runs can vary by 10–15 points

Step 2: Implement fixes

Apply each fix and deploy. Don’t implement all changes simultaneously if you want to measure individual impact.

Step 3: Run post-fix test

Same process — 3 runs, take median. Compare to baseline.

Step 4: Monitor field data

Lab scores (PageSpeed) are immediate. Field data in Google Search Console → Core Web Vitals takes 28 days of traffic to update. Check back monthly. If your lab score is 90+ but field data still shows failing, the issues are in real user environments (specific devices, network conditions) that PageSpeed doesn’t fully simulate.

The Performance-SEO Connection

Core Web Vitals are a ranking factor, but their indirect effects on SEO are equally important.

A slow site with high LCP increases bounce rate. Users click your result from Google, wait 6 seconds for content, and hit back. Google’s Pogo-sticking signal — users returning quickly to search results — is a quality signal that suppresses your rankings.

A site with high CLS creates a frustrating user experience where content jumps as users try to click. This leads to accidental clicks, frustration, and again, quick exits.

Both patterns tell Google: users don’t find what they need here. Pages with those signals rank lower, regardless of content quality.

Fixing Core Web Vitals is therefore not just a PageSpeed optimization — it’s fixing signals that directly affect how Google evaluates whether users are satisfied with your page.

If you’d rather have an expert run these optimizations than spend a week on them yourself, our Fix + Rank Package includes full Core Web Vitals optimization as part of the technical SEO implementation. Most clients see scores above 85 on mobile within the project timeline.

Frequently Asked Questions

Why do Lovable sites score low on PageSpeed?

Lovable builds React single-page applications that ship as large JavaScript bundles with no code splitting, no image optimization, and no server-side rendering. The browser must download and execute 400KB–1.5MB of JavaScript before displaying content, causing high LCP. Most unoptimized Lovable sites score 20–40 on mobile.

What is a good Core Web Vitals score for 2025?

Google’s passing thresholds: LCP under 2.5 seconds, CLS under 0.1, INP under 200 milliseconds. On PageSpeed Insights, a score of 90+ on mobile indicates passing Core Web Vitals for most pages. Target 85+ minimum, 90+ ideally.

Do Core Web Vitals affect Google rankings?

Yes. Google confirmed Core Web Vitals as a ranking signal in 2021 and updated the metric set to include INP in March 2024. The impact is a ranking modifier — pages failing Core Web Vitals are suppressed relative to pages passing them when other signals are equal.

How do I check Core Web Vitals for my vibe-coded site?

Run your URL through Google PageSpeed Insights at pagespeed.web.dev for lab data. For real user data, check Google Search Console → Experience → Core Web Vitals. Always test on mobile — Google uses mobile-first indexing.

Can I fix Core Web Vitals on a Lovable site without rebuilding?

Yes. Image optimization, code splitting, font optimization, and script deferral can all be applied without changing the site’s functionality or design. These fixes alone typically move Lovable sites from 20–40 to 75–88 on mobile PageSpeed.

What causes layout shift (CLS) on vibe-coded sites?

The most common causes: images without explicit width/height attributes (browser doesn’t know how much space to reserve), dynamic content that renders at different heights than its loading state, and fonts that load and swap, pushing text around. All are fixable with explicit dimensions and font-display: swap.

Work directly with me.

Get a production-first technical review with documented findings, priorities, and next steps.

Get a Free Audit