AiTechWorlds
AiTechWorlds
Core Web Vitals are Google's official metrics for measuring user experience. They directly affect SEO rankings and, more importantly, how your app feels to use. This lesson covers what each metric means and the specific Next.js techniques that improve them.
LCP — Largest Contentful Paint: How long until the largest element (usually a hero image or heading) is visible. Target: under 2.5 seconds.
INP — Interaction to Next Paint (replaced FID in 2024): How quickly the page responds to user input. Target: under 200ms.
CLS — Cumulative Layout Shift: How much the page layout jumps around as it loads. Target: under 0.1.
# Install the Lighthouse CLI
npm install -g lighthouse
# Test a production URL
lighthouse https://yourapp.com --output html --view
# Or use Vercel Speed Insights (real user data)
npm install @vercel/speed-insights
Also check:
import Image from "next/image";
// ✅ next/image handles:
// - Automatic WebP/AVIF conversion
// - Responsive srcset generation
// - Lazy loading by default
// - Size optimization
function CourseCover({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={1280}
height={720}
className="rounded-xl object-cover"
priority // Add to hero/LCP images — preloads them
/>
);
}
// For above-the-fold images (hero, course banner), add priority
<Image src="/hero.jpg" alt="Hero" fill priority />
priority triggers a <link rel="preload"> in the document <head>. The browser fetches the image immediately instead of waiting for the JavaScript to render.
// src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
{/* Preconnect to CDN, fonts, APIs */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://images.ctfassets.net" />
</head>
<body>{children}</body>
</html>
);
}
// next/font downloads fonts at build time, zero network request at runtime
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap", // Text visible immediately (swap)
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
);
}
Google Fonts loaded via next/font are embedded in your CSS — no separate HTTP request, no flash of invisible text.
// Lazy load components not needed immediately
import { lazy, Suspense } from "react";
const HeavyEditor = lazy(() => import("./HeavyEditor"));
const ChartDashboard = lazy(() => import("./ChartDashboard"));
function CoursePage() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<button onClick={() => setShowEditor(true)}>Edit Course</button>
{showEditor && (
<Suspense fallback={<div>Loading editor...</div>}>
<HeavyEditor />
</Suspense>
)}
</div>
);
}
// ❌ Synchronous heavy computation blocks UI
function SearchResults({ items }: { items: Item[] }) {
// This runs synchronously and blocks rendering
const filtered = expensiveFilter(items); // 200ms of CPU work
return <ul>{filtered.map(...)}</ul>;
}
// ✅ useMemo prevents recomputation, startTransition defers non-urgent updates
function SearchResults({ items, query }: { items: Item[]; query: string }) {
const filtered = useMemo(() => expensiveFilter(items, query), [items, query]);
return <ul>{filtered.map(...)}</ul>;
}
// ✅ For very expensive operations, use useTransition
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleSearch(value: string) {
setQuery(value); // Update input immediately
startTransition(() => {
setResults(heavyFilter(data, value)); // Defer expensive update
});
}
}
Next.js automatically code-splits at route level — each page loads its own bundle. But you can further split large third-party libraries:
// src/app/admin/analytics/page.tsx — analytics library only loaded for /admin/analytics
import dynamic from "next/dynamic";
const AnalyticsChart = dynamic(() => import("recharts").then(m => m.LineChart), {
ssr: false, // Charts often need browser APIs
loading: () => <ChartSkeleton />,
});
// ❌ Layout shifts as image loads (browser doesn't know the size)
<img src="/course.jpg" alt="Course" className="w-full" />
// ✅ Dimensions prevent layout shift
<Image src="/course.jpg" alt="Course" width={640} height={360} />
// ✅ Or use aspect-ratio container
<div className="relative w-full aspect-video">
<Image src="/course.jpg" alt="Course" fill className="object-cover" />
</div>
// ❌ Skeleton is the wrong size — content shifts layout when it loads
<div className="h-4 w-32 animate-pulse bg-gray-200 rounded" />
// Actual content: a long 3-line paragraph
// ✅ Skeleton matches content dimensions
function TitleSkeleton() {
return (
<div className="space-y-2">
<div className="h-8 w-2/3 animate-pulse bg-gray-200 rounded" />
<div className="h-4 w-full animate-pulse bg-gray-200 rounded" />
<div className="h-4 w-4/5 animate-pulse bg-gray-200 rounded" />
</div>
);
}
// ❌ Toast appears and pushes content down
// ✅ Position toasts as fixed overlays, not in document flow
// ❌ Cookie banner appears at top and shifts everything
// ✅ Use a sticky banner with reserved height, or load it from cookies on server
# Add bundle analyzer
npm install @next/bundle-analyzer
# next.config.ts
import BundleAnalyzer from "@next/bundle-analyzer";
const withBundleAnalyzer = BundleAnalyzer({
enabled: process.env.ANALYZE === "true",
});
export default withBundleAnalyzer(config);
# Run
ANALYZE=true npm run build
The analyzer shows a visual treemap of your bundle. Look for:
Route (app) Size First Load JS
┌ ○ / 2.4 kB 87.4 kB
├ ○ /about 1.2 kB 86.2 kB
├ ● /blog/[slug] 3.1 kB 88.1 kB
├ λ /dashboard 4.2 kB 89.2 kB
└ λ /api/courses 0 B 0 B
○ Static ● ISR/SSG λ Dynamic (server-rendered)
○ Static — served from CDN, fastest● ISR — cached but can revalidateλ Dynamic — server-rendered per requestAim to have public-facing pages be static or ISR. Move dynamic rendering only where truly required (authenticated pages, real-time data).
Congratulations — you've completed the React/Next.js Complete Course. You're now equipped to build production-quality Next.js applications.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises