Fix LCP in Vue 3
LCP in Vue 3 apps built with Vite is most commonly caused by hero images loaded without priority hints, oversized initial JavaScript bundles that delay first render, and all route components bundled into a single file. Vue's composition API and Vite's bundler are both performance-friendly, but the build configuration determines how much JavaScript the browser must parse before painting the LCP element. These five fixes address the highest-impact causes.
Expected results
Before
3.6s
LCP (Poor) -- no preload, all routes in one bundle, unoptimized images
After
1.4s
LCP (Good) -- preloaded hero, lazy routes, split vendor chunks
Step-by-step fix
Preload the hero image with fetchpriority
The browser discovers images referenced inside Vue components late -- after it downloads, parses, and executes JavaScript. For the LCP image, add a rel="preload" link directly in index.html so the browser starts fetching it from the HTML stream. Always add fetchpriority="high" on the <img> element and never use loading="lazy" on the LCP candidate.
<!-- index.html: preload static LCP image at document level -->
<link
rel="preload"
as="image"
href="/images/hero.webp"
fetchpriority="high"
/>
<!-- For responsive heroes, preload the correct srcset -->
<link
rel="preload"
as="image"
imagesrcset="/hero-480.webp 480w, /hero-800.webp 800w, /hero-1200.webp 1200w"
imagesizes="100vw"
fetchpriority="high"
/>
<!-- Vue component: set fetchpriority, disable lazy loading -->
<template>
<img
src="/images/hero.webp"
alt="Product hero"
width="1200"
height="600"
fetchpriority="high"
loading="eager"
decoding="async"
/>
</template>
Split routes with dynamic imports
Statically importing all Vue Router views at the top of router/index.js includes every page's component tree in the initial bundle. Switching to dynamic imports makes Vite create a separate JS chunk per route. The browser only downloads the current route's chunk, reducing initial parse time from seconds to milliseconds.
// router/index.js
// Bad: all views in the initial bundle
import Home from '@/views/Home.vue';
import Dashboard from '@/views/Dashboard.vue';
import ProductPage from '@/views/ProductPage.vue';
// Good: each route loaded on demand
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
// Vite splits this into home.[hash].js
component: () => import('@/views/Home.vue'),
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
},
{
path: '/products/:id',
component: () => import('@/views/ProductPage.vue'),
},
];
export default createRouter({
history: createWebHistory(),
routes,
});
Optimize Vite's chunk splitting
Vite's default behavior creates a single large vendor chunk mixing all node_modules dependencies. Splitting this into separately-named chunks (Vue core, UI libraries, utilities) enables aggressive long-term caching -- each chunk only invalidates when its specific library updates. Add manualChunks to vite.config.js.
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
// Vue runtime: rarely updates, cache indefinitely
if (
id.includes('node_modules/vue/') ||
id.includes('node_modules/@vue/') ||
id.includes('node_modules/vue-router/') ||
id.includes('node_modules/pinia/')
) {
return 'vue-vendor';
}
// Heavy UI framework in its own chunk
if (id.includes('node_modules/@headlessui/')) {
return 'ui-vendor';
}
// Remaining node_modules
if (id.includes('node_modules/')) {
return 'vendor';
}
},
},
},
chunkSizeWarningLimit: 200,
},
});
Add preconnect hints for external origins
If the LCP element depends on resources from external origins -- Google Fonts, an image CDN, or an API server -- the browser must complete a full DNS lookup, TCP handshake, and TLS negotiation (300-500ms combined) before fetching the first byte. rel="preconnect" starts those connections in parallel with other work, eliminating the sequential wait.
<!-- index.html <head> -->
<!-- Font providers -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Image CDN -->
<link rel="preconnect" href="https://cdn.yourdomain.com">
<!-- API server (if LCP depends on a network call) -->
<link rel="preconnect" href="https://api.yourdomain.com">
<!-- dns-prefetch as fallback for older browsers -->
<link rel="dns-prefetch" href="https://cdn.yourdomain.com">
<!-- Limit preconnects to 2-3 critical origins.
Too many preconnects compete for bandwidth and backfire -->
Serve images in WebP with explicit dimensions
JPEG and PNG files are 2-5x larger than equivalent WebP or AVIF images. Smaller files finish downloading sooner, directly reducing LCP. Additionally, images without width and height attributes cause layout reflow as they load (CLS) and miss the browser's aspect-ratio reservation that improves LCP timing. Use the <picture> element for format negotiation.
<template>
<!-- Use <picture> for AVIF with WebP fallback -->
<picture>
<source
srcset="/hero-480.avif 480w, /hero-1200.avif 1200w"
sizes="100vw"
type="image/avif"
/>
<source
srcset="/hero-480.webp 480w, /hero-1200.webp 1200w"
sizes="100vw"
type="image/webp"
/>
<img
src="/hero-1200.jpg"
alt="Hero image"
width="1200"
height="600"
fetchpriority="high"
loading="eager"
/>
</picture>
</template>
<!-- Vite plugin for automatic image optimization -->
<!-- npm install vite-plugin-imagemin -->
<!-- vite.config.js: import viteImagemin from 'vite-plugin-imagemin' -->
Quick checklist
-
Hero image has
rel="preload"inindex.htmlandfetchpriority="high"on the<img> -
All Vue Router routes use
() => import()dynamic imports -
manualChunksseparates Vue core, router, and UI libraries into named chunks - Preconnect hints added for fonts, CDN, and API origins
-
Hero image served in WebP/AVIF with explicit
widthandheightattributes -
LCP element is not inside a lazy-loaded component or behind a
v-ifon first render
Frequently asked questions
The most common causes in Vue 3 (Vite) apps are: hero images discovered late because they are rendered by a Vue component rather than being in the initial HTML, large JavaScript bundles that delay the first render, all route components bundled together, missing preconnect hints for external origins, and images served in JPEG or PNG without dimension attributes.
Vue 3's reactivity system has minimal impact on LCP. The Proxy-based system is fast, and with tree-shaking in Vite the Vue runtime is under 20KB. The biggest LCP wins come from bundle splitting and image optimizations -- not from the reactivity overhead.
Nuxt adds SSR and SSG which significantly improve LCP by delivering fully-rendered HTML rather than a blank page. For content-heavy sites with a predictable LCP element, Nuxt's SSG mode produces the best scores. For highly interactive SPAs where SSR is impractical, the Vite optimizations in this guide are the best approach without a framework switch.
Use Chrome DevTools Performance tab with 4x CPU throttling and Slow 4G network. For consistent lab measurements, run Lighthouse from the command line. For field data, install the web-vitals npm package and call onLCP() in your main.js to capture real user LCP values and send them to your analytics endpoint.
defineAsyncComponent is ideal for below-the-fold or interaction-triggered components. For the component containing the LCP element itself, keep it in the synchronous initial bundle so it renders immediately. Use defineAsyncComponent for sidebars, modals, and anything the user will not see on first load.
Google rates LCP as 'good' when it is under 2.5 seconds at the 75th percentile. For Vue applications specifically, aim for under 2.0 seconds. Measure with field data from Chrome User Experience Report (CrUX) through PageSpeed Insights, as lab tests may not reflect real-user experience with third-party scripts and varying network conditions.
Related resources
Complete LCP Guide
Deep dive into LCP measurement, thresholds, element types, and optimization strategies for any stack.
FixFix LCP in React
React-specific LCP fixes including resource hints, lazy loading components, and code splitting strategies.
FixFix LCP in Next.js
Next.js fixes using next/image priority, SSG, and React Server Components for faster LCP.
Continue learning
Complete LCP Guide
Deep dive into LCP -- thresholds, measurement, and optimization strategies.
FixFix CLS in Vue
Related performance optimization for the same framework.
FixFix INP in Vue
Related performance optimization for the same framework.
ToolCWV Score Explainer
Enter your scores for personalized fix recommendations.