Core Web Vitals by Framework
Optimizing LCP (Largest Contentful Paint)
The LCP element is typically a hero image or large text block. Each framework provides optimized image components.- Next.js
- Nuxt
- Astro
- React
- Vue
- Svelte
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/hero.jpg"
priority // Preloads with high priority
fill // Fills container
sizes="100vw" // Responsive sizing
alt="Hero image"
/>
);
}
- Automatic WebP/AVIF generation
- Built-in lazy loading (disabled with
priority) - Automatic responsive images via
sizes - Image optimization at build time
<template>
<NuxtImg
src="/hero.jpg"
preload
loading="eager"
sizes="100vw"
alt="Hero image"
/>
</template>
- Uses
@nuxt/imagemodule - Automatic format optimization
- CDN integration support
- Responsive image generation
---
import { Image } from 'astro:assets';
import hero from '../assets/hero.jpg';
---
<Image
src={hero}
loading="eager"
decoding="sync"
alt="Hero image"
widths={[400, 800, 1200]}
sizes="100vw"
/>
- Built-in image optimization
- Multiple format generation
- Type-safe image imports
- Automatic width generation
export default function Hero() {
return (
<>
{/* Preload in head */}
<link
rel="preload"
href="/hero.jpg"
as="image"
fetchpriority="high"
/>
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img
src="/hero.jpg"
width="1200"
height="600"
fetchpriority="high"
alt="Hero image"
/>
</picture>
</>
);
}
- Create multiple formats (AVIF, WebP, JPEG)
- Add preload hints manually
- Use
<picture>for format fallbacks
<template>
<picture>
<source :srcset="heroAvif" type="image/avif" />
<source :srcset="heroWebp" type="image/webp" />
<img
:src="heroJpg"
:style="{ aspectRatio: '16/9' }"
width="1200"
height="600"
alt="Hero image"
/>
</picture>
</template>
<script setup>
const heroAvif = '/hero.avif';
const heroWebp = '/hero.webp';
const heroJpg = '/hero.jpg';
</script>
- Use computed properties for dynamic image paths
- Bind aspect-ratio to prevent CLS
- Optimize images with build tools
<script>
const images = {
avif: '/hero.avif',
webp: '/hero.webp',
jpg: '/hero.jpg'
};
</script>
<picture>
<source srcset={images.avif} type="image/avif" />
<source srcset={images.webp} type="image/webp" />
<img
src={images.jpg}
width="1200"
height="600"
alt="Hero image"
style="aspect-ratio: 16/9;"
/>
</picture>
- Reactive image switching
- Minimal runtime overhead
- Easy style binding
Optimizing INP (Interaction to Next Paint)
INP measures responsiveness. Heavy event handlers and re-renders are common culprits.- React
- Vue
- Svelte
- Next.js
import { useState, useTransition, useMemo } from 'react';
function ExpensiveList({ items }) {
const [filter, setFilter] = useState('');
const [isPending, startTransition] = useTransition();
// Defer non-urgent updates
const handleFilterChange = (e) => {
startTransition(() => {
setFilter(e.target.value);
});
};
// Memoize expensive computations
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<>
<input
onChange={handleFilterChange}
placeholder="Filter..."
/>
{isPending && <div>Updating...</div>}
<List items={filteredItems} />
</>
);
}
// Prevent unnecessary re-renders
const List = React.memo(({ items }) => {
return items.map(item => <Item key={item.id} item={item} />);
});
useTransitionfor non-urgent updatesuseMemofor expensive calculationsReact.memoto prevent re-rendersuseCallbackfor stable function references
<template>
<div>
<input v-model="filter" placeholder="Filter..." />
<TransitionGroup>
<ListItem
v-for="item in filteredItems"
:key="item.id"
:item="item"
/>
</TransitionGroup>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const filter = ref('');
const items = ref([/* ... */]);
// Computed properties are cached
const filteredItems = computed(() => {
return items.value.filter(item =>
item.name.toLowerCase().includes(filter.value.toLowerCase())
);
});
</script>
<script>
// Use v-once for static content
// Use v-memo for conditional caching
</script>
- Computed properties for caching
v-oncefor static contentv-memofor conditional memoization- Async components for code splitting
<script>
let filter = '';
let items = [/* ... */];
// Reactive statement (auto-memoized)
$: filteredItems = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
// Debounce for expensive operations
import { debounce } from './utils';
const debouncedFilter = debounce((value) => {
filter = value;
}, 300);
</script>
<input
on:input={(e) => debouncedFilter(e.target.value)}
placeholder="Filter..."
/>
{#each filteredItems as item (item.id)}
<ListItem {item} />
{/each}
- Reactive statements (
$:) are auto-memoized - Minimal runtime overhead
- Built-in keyed each blocks
- Easy event handler optimization
'use client'; // Mark as client component
import { useState, useTransition } from 'react';
import dynamic from 'next/dynamic';
// Lazy load heavy components
const HeavyChart = dynamic(() => import('./HeavyChart'), {
ssr: false,
loading: () => <div>Loading chart...</div>
});
export default function Dashboard() {
const [showChart, setShowChart] = useState(false);
const [isPending, startTransition] = useTransition();
const handleToggle = () => {
startTransition(() => {
setShowChart(!showChart);
});
};
return (
<>
<button onClick={handleToggle}>Toggle Chart</button>
{isPending && <div>Loading...</div>}
{showChart && <HeavyChart />}
</>
);
}
dynamic()for code splitting- Server Components reduce client JS
useTransitionfor smooth interactions- Streaming with Suspense
Optimizing CLS (Cumulative Layout Shift)
Prevent unexpected layout shifts by reserving space for dynamic content.- Next.js
- All Frameworks
import Image from 'next/image';
// Image component automatically prevents CLS
<Image
src="/photo.jpg"
width={800}
height={600}
alt="Photo"
/>
// For dynamic content, reserve space
<div style={{ minHeight: '200px' }}>
{loading ? <Skeleton /> : <Content />}
</div>
// Use CSS aspect-ratio for responsive layouts
<div style={{ aspectRatio: '16/9', width: '100%' }}>
<iframe src="..." />
</div>
// Set dimensions on all images
<img src="photo.jpg" width="800" height="600" alt="Photo" />
// Or use aspect-ratio
<img
src="photo.jpg"
style={{ aspectRatio: '4/3', width: '100%' }}
alt="Photo"
/>
// Reserve space for ads/embeds
<div style={{ minHeight: '250px' }}>
<AdComponent />
</div>
// Use font-display to prevent font shifts
<style>
@font-face {
font-family: 'Custom';
src: url('/font.woff2') format('woff2');
font-display: optional; // or swap with size-adjust
}
</style>
Code Splitting & Lazy Loading
- React
- Next.js
- Vue
- Nuxt
- Svelte
- Astro
import { lazy, Suspense } from 'react';
// Component-level code splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
);
}
// Route-based splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
import dynamic from 'next/dynamic';
// Component splitting with SSR disabled
const DynamicComponent = dynamic(
() => import('./HeavyComponent'),
{
ssr: false,
loading: () => <LoadingSpinner />
}
);
// Automatic route-based splitting
// pages/dashboard.js automatically code-split
<script setup>
import { defineAsyncComponent } from 'vue';
// Async component
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
</script>
<template>
<Suspense>
<template #default>
<HeavyComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
<template>
<!-- Automatic code splitting -->
<LazyHeavyComponent v-if="showComponent" />
</template>
<script setup>
// Components prefixed with 'Lazy' are auto-lazy-loaded
const showComponent = ref(false);
</script>
<script>
let Component;
let loading = false;
async function loadComponent() {
loading = true;
const module = await import('./HeavyComponent.svelte');
Component = module.default;
loading = false;
}
</script>
<button on:click={loadComponent}>Load Component</button>
{#if loading}
<LoadingSpinner />
{:else if Component}
<svelte:component this={Component} />
{/if}
---
// Partial hydration - load JS only when needed
import HeavyComponent from './HeavyComponent.jsx';
---
<!-- Only loads JS when visible -->
<HeavyComponent client:visible />
<!-- Only loads JS when idle -->
<InteractiveWidget client:idle />
<!-- Only loads JS on media query match -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- Never loads JS (static only) -->
<StaticContent />
Error Handling
- React
- Vue
- Svelte
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
// Log to error tracking service
console.error('Error:', error, info);
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<App />
</ErrorBoundary>
<script setup>
import { onErrorCaptured } from 'vue';
onErrorCaptured((err, instance, info) => {
console.error('Error captured:', err, info);
// Return false to prevent propagation
return false;
});
</script>
<!-- Global error handler in main.js -->
<script>
app.config.errorHandler = (err, instance, info) => {
console.error('Global error:', err);
};
</script>
<script>
import { onMount } from 'svelte';
let error = null;
onMount(() => {
window.addEventListener('error', (e) => {
error = e.error;
});
});
</script>
{#if error}
<ErrorDisplay {error} />
{:else}
<App />
{/if}
SEO & Meta Tags
- Next.js
- Nuxt
- Astro
- React/Vue
import Head from 'next/head';
export default function Page() {
return (
<>
<Head>
<title>Page Title - Site Name</title>
<meta
name="description"
content="Compelling page description"
/>
<meta property="og:title" content="Page Title" />
<meta property="og:description" content="..." />
<link rel="canonical" href="https://example.com/page" />
</Head>
<main>{/* Content */}</main>
</>
);
}
// Or use Next.js 13+ Metadata API
export const metadata = {
title: 'Page Title',
description: 'Page description',
};
<script setup>
useHead({
title: 'Page Title - Site Name',
meta: [
{
name: 'description',
content: 'Compelling page description'
},
{
property: 'og:title',
content: 'Page Title'
}
],
link: [
{
rel: 'canonical',
href: 'https://example.com/page'
}
]
});
</script>
---
const title = 'Page Title - Site Name';
const description = 'Compelling page description';
---
<html lang="en">
<head>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<link rel="canonical" href="https://example.com/page" />
</head>
<body>
<slot />
</body>
</html>
// Use react-helmet or @vueuse/head
import { Helmet } from 'react-helmet';
function Page() {
return (
<>
<Helmet>
<title>Page Title - Site Name</title>
<meta name="description" content="..." />
<link rel="canonical" href="https://example.com/page" />
</Helmet>
<main>{/* Content */}</main>
</>
);
}
Related Resources
Core Web Vitals
Deep dive into LCP, INP, and CLS optimization
Performance
Comprehensive performance optimization guide