Theory भूलिए — मैंने एक real Next.js production site पर LCP 2.5s के नीचे, CLS zero, और INP 200ms के नीचे लाने के लिए असल में क्या किया। Specific techniques, vague advice नहीं।
मैंने इस site को तेज बनाने में करीब दो हफ्ते बिताए। "मेरे M3 MacBook पर Lighthouse audit में तेज दिखता है" वाला तेज नहीं। वाकई तेज। $150 के Android phone पर हिलते हुए 4G connection पर subway tunnel में तेज। वहां तेज जहां मायने रखता है।
नतीजा: LCP 1.8s से नीचे, CLS 0.00 पर, INP 120ms से नीचे। तीनों CrUX data में green, सिर्फ lab scores नहीं। और मैंने इस प्रक्रिया में कुछ सीखा — internet पर ज्यादातर performance advice या तो पुरानी है, अस्पष्ट है, या दोनों।
"अपनी images optimize करें" advice नहीं है। Context के बिना "lazy loading use करें" खतरनाक है। "JavaScript minimize करें" obvious है लेकिन ये नहीं बताता कि क्या काटें।
यहां बताया गया है कि मैंने वास्तव में क्या किया, उस क्रम में जो मायने रखता था।
सीधे बात करता हूं: Google Core Web Vitals को ranking signal के रूप में use करता है। एकमात्र signal नहीं, और सबसे महत्वपूर्ण भी नहीं। Content relevance, backlinks, और domain authority अभी भी dominant हैं। लेकिन margins पर — जहां दो pages में comparable content और authority है — performance tiebreaker है। और internet पर, करोड़ों pages उन margins पर रहते हैं।
लेकिन एक पल के लिए SEO भूलिए। Performance की परवाह करने की असली वजह users हैं। Data पिछले पांच सालों में ज्यादा नहीं बदला:
2026 में Core Web Vitals तीन metrics से बने हैं:
| Metric | क्या मापता है | अच्छा | सुधार चाहिए | खराब |
|---|---|---|---|---|
| LCP | Loading performance | ≤ 2.5s | 2.5s – 4.0s | > 4.0s |
| CLS | Visual stability | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |
| INP | Responsiveness | ≤ 200ms | 200ms – 500ms | > 500ms |
ये thresholds मार्च 2024 में INP ने FID की जगह लेने के बाद से नहीं बदले हैं। लेकिन इन्हें हासिल करने की techniques विकसित हुई हैं, खासकर React/Next.js ecosystem में।
Largest Contentful Paint मापता है कि viewport में सबसे बड़ा visible element कब rendering पूरी करता है। ज्यादातर pages के लिए, ये hero image, heading, या text का बड़ा block होता है।
कुछ भी optimize करने से पहले, आपको जानना होगा कि आपका LCP element क्या है। लोग मान लेते हैं कि ये उनकी hero image है। कभी-कभी ये <h1> render करने वाला web font होता है। कभी-कभी CSS से apply की गई background image होती है। कभी-कभी <video> poster frame होता है।
Chrome DevTools खोलें, Performance panel पर जाएं, page load record करें, और "LCP" marker देखें। ये बिल्कुल बताता है कि किस element ने LCP trigger किया।
आप web-vitals library use करके programmatically भी log कर सकते हैं:
import { onLCP } from "web-vitals";
onLCP((metric) => {
console.log("LCP element:", metric.entries[0]?.element);
console.log("LCP value:", metric.value, "ms");
});इस site पर, LCP element homepage पर hero image और blog posts पर text का पहला paragraph निकला। दो अलग elements, दो अलग optimization strategies।
अगर आपका LCP element image है, तो सबसे प्रभावशाली चीज जो आप कर सकते हैं वो है इसे preload करना। Default रूप से, browser images को तब discover करता है जब वो HTML parse करता है, जिसका मतलब image request HTML download, parse, और <img> tag तक पहुंचने के बाद ही शुरू होती है। Preloading उस discovery को एकदम शुरुआत में ले जाता है।
Next.js में, आप अपने layout या page में preload link जोड़ सकते हैं:
import Head from "next/head";
export default function HeroSection() {
return (
<>
<Head>
<link
rel="preload"
as="image"
href="/images/hero-optimized.webp"
type="image/webp"
fetchPriority="high"
/>
</Head>
<section className="relative h-[600px]">
<img
src="/images/hero-optimized.webp"
alt="Hero banner"
width={1200}
height={600}
fetchPriority="high"
decoding="sync"
/>
</section>
</>
);
}fetchPriority="high" पर ध्यान दें। ये newer Fetch Priority API है, और ये game changer है। इसके बिना, browser resources prioritize करने के लिए अपनी heuristics use करता है — और वो heuristics अक्सर गलत होती हैं, खासकर जब आपके पास above the fold कई images हों।
इस site पर, LCP image में fetchPriority="high" जोड़ने से LCP ~400ms गिरा। ये एक-line change से मिली सबसे बड़ी जीत है।
CSS rendering block करती है। सारी। अगर आपके पास <link rel="stylesheet"> से loaded 200KB stylesheet है, तो browser पूरी download और parse होने तक कुछ भी paint नहीं करेगा।
Fix तीन तरफा है:
Critical CSS inline करें — Above-the-fold content के लिए जरूरी CSS extract करें और <head> में <style> tag में inline करें। Next.js ये automatically करता है जब आप CSS Modules या proper purging के साथ Tailwind use करते हैं।
Non-critical CSS defer करें — अगर आपके पास below-the-fold content (footer animation library, chart component) के लिए stylesheets हैं, तो इन्हें asynchronously load करें:
<link
rel="preload"
href="/styles/charts.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript>
<link rel="stylesheet" href="/styles/charts.css" />
</noscript>LCP तेज नहीं हो सकता अगर TTFB धीमा है। अगर आपका server respond करने में 800ms लेता है, तो आपका LCP कम से कम 800ms + बाकी सब कुछ होगा।
इस site पर (Node.js + PM2 + Nginx एक VPS पर), मैंने cold hit पर TTFB लगभग 180ms मापा। यहां बताया गया है कि मैंने इसे वहीं रखने के लिए क्या किया:
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400।# nginx.conf snippet
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000;
gzip_comp_level 6;
# Brotli (अगर ngx_brotli module installed है)
brotli on;
brotli_types text/plain text/css application/json application/javascript text/xml;
brotli_comp_level 6;LCP पर मेरा पहले/बाद:
Cumulative Layout Shift मापता है कि page load के दौरान visible content कितना हिलता-डुलता है। 0 की CLS का मतलब कुछ shift नहीं हुआ। 0.1 से ऊपर CLS का मतलब कुछ आपके users को visually परेशान कर रहा है।
CLS वो metric है जिसे ज्यादातर developers कम आंकते हैं। आप इसे अपनी तेज development machine पर सब cached होने पर notice नहीं करते। आपके users इसे अपने phones पर, धीमे connections पर notice करते हैं, जहां fonts देर से load होते हैं और images एक-एक करके pop in होती हैं।
1. बिना explicit dimensions वाली Images
ये सबसे आम CLS कारण है। जब image load होती है, तो ये उसके नीचे के content को push करती है। Fix शर्मनाक रूप से simple है: हमेशा <img> tags पर width और height specify करें।
// खराब — layout shift करता है
<img src="/photo.jpg" alt="Team photo" />
// अच्छा — browser image load होने से पहले space reserve करता है
<img src="/photo.jpg" alt="Team photo" width={800} height={450} />अगर आप Next.js <Image> use कर रहे हैं, तो जब तक आप dimensions provide करते हैं या sized parent container के साथ fill use करते हैं, ये automatically handle होता है।
लेकिन यहां gotcha है: अगर आप fill mode use करते हैं, तो parent container में explicit dimensions होने ही चाहिए वरना image CLS करेगी:
// खराब — parent में dimensions नहीं हैं
<div className="relative">
<Image src="/photo.jpg" alt="Team" fill />
</div>
// अच्छा — parent में explicit aspect ratio है
<div className="relative aspect-video w-full">
<Image src="/photo.jpg" alt="Team" fill sizes="100vw" />
</div>2. FOUT/FOIT करने वाले Web fonts
जब custom font load होता है, तो fallback font में rendered text custom font में re-render होता है। अगर दोनों fonts की metrics अलग हैं (लगभग हमेशा होती हैं), तो सब कुछ shift होता है।
Modern fix है font-display: swap combined with size-adjusted fallback fonts:
// next/font use करना — Next.js के लिए सबसे अच्छा approach
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
// next/font automatically size-adjusted fallback fonts generate करता है
// ये font swapping से CLS eliminate करता है
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}next/font वाकई Next.js की सबसे अच्छी चीजों में से एक है। ये build time पर fonts download करता है, self-host करता है (runtime पर कोई Google Fonts request नहीं), और size-adjusted fallback fonts generate करता है ताकि fallback से custom font में swap करने पर zero layout shift हो। मैंने next/font पर switch करने के बाद fonts से CLS 0.00 मापी। पहले, standard Google Fonts <link> के साथ, ये 0.04-0.08 थी।
3. Dynamic content injection
Ads, cookie banners, notification bars — कोई भी चीज जो initial render के बाद DOM में inject होती है, CLS करती है अगर वो content को नीचे push करती है।
Fix: dynamic content के load होने से पहले space reserve करें।
// Cookie banner — नीचे space reserve करें
function CookieBanner() {
const [accepted, setAccepted] = useState(false);
if (accepted) return null;
return (
// Fixed positioning CLS नहीं करती क्योंकि ये
// document flow को affect नहीं करती
<div className="fixed bottom-0 left-0 right-0 z-50 bg-gray-900 p-4">
<p>We use cookies. You know the drill.</p>
<button onClick={() => setAccepted(true)}>Accept</button>
</div>
);
}Dynamic elements के लिए position: fixed या position: absolute use करना CLS-free approach है क्योंकि ये elements normal document flow को affect नहीं करते।
4. aspect-ratio CSS trick
Responsive containers के लिए जहां आपको aspect ratio पता है लेकिन exact dimensions नहीं, CSS aspect-ratio property use करें:
// बिना CLS के Video embed
function VideoEmbed({ src }: { src: string }) {
return (
<div className="w-full aspect-video bg-gray-900 rounded-lg overflow-hidden">
<iframe
src={src}
className="w-full h-full"
title="Embedded video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media"
allowFullScreen
/>
</div>
);
}aspect-video utility (जो aspect-ratio: 16/9 है) बिल्कुल सही मात्रा में space reserve करता है। Iframe load होने पर कोई shift नहीं।
5. Skeleton screens
Asynchronously load होने वाले content (API data, dynamic components) के लिए, expected dimensions से मिलता-जुलता skeleton दिखाएं:
function PostCardSkeleton() {
return (
<div className="animate-pulse rounded-lg border p-4">
<div className="h-48 w-full rounded bg-gray-200" />
<div className="mt-4 space-y-2">
<div className="h-6 w-3/4 rounded bg-gray-200" />
<div className="h-4 w-full rounded bg-gray-200" />
<div className="h-4 w-5/6 rounded bg-gray-200" />
</div>
</div>
);
}
function PostList() {
const { data: posts, isLoading } = usePosts();
if (isLoading) {
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<PostCardSkeleton key={i} />
))}
</div>
);
}
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{posts?.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}मुख्य बात ये है कि PostCardSkeleton और PostCard की dimensions एक जैसी होनी चाहिए। अगर skeleton 200px ऊंचा है और actual card 280px ऊंचा है, तो फिर भी shift होगी।
मेरे CLS results:
Interaction to Next Paint ने मार्च 2024 में First Input Delay की जगह ली, और ये fundamentally कठिन metric है optimize करने के लिए। FID सिर्फ पहली interaction process होने से पहले delay मापता था। INP पूरी page lifecycle में हर interaction मापता है और worst (75th percentile पर) report करता है।
इसका मतलब page की FID अच्छी हो सकती है लेकिन INP भयानक, अगर, कहें, load होने के 30 seconds बाद dropdown menu click करने पर 500ms reflow trigger होता है।
offsetHeight) read करना फिर उन्हें (जैसे style.height बदलना) loop में write करना browser को synchronously layout recalculate करने पर मजबूर करता है।INP के लिए सबसे प्रभावशाली technique long tasks तोड़ना है ताकि browser chunks के बीच user interactions process कर सके:
async function processLargeDataset(items: DataItem[]) {
const results: ProcessedItem[] = [];
for (let i = 0; i < items.length; i++) {
results.push(expensiveTransform(items[i]));
// हर 10 items पर, browser को yield करें
// ये pending user interactions को process होने देता है
if (i % 10 === 0 && "scheduler" in globalThis) {
await scheduler.yield();
}
}
return results;
}scheduler.yield() Chrome 129+ (September 2024) में उपलब्ध है और main thread को yield करने का recommended तरीका है। जो browsers support नहीं करते, उनके लिए setTimeout(0) wrapper fallback कर सकते हैं:
function yieldToMain(): Promise<void> {
if ("scheduler" in globalThis && "yield" in scheduler) {
return scheduler.yield();
}
return new Promise((resolve) => setTimeout(resolve, 0));
}React 18+ हमें useTransition देता है, जो React को बताता है कि कुछ state updates urgent नहीं हैं और ज्यादा महत्वपूर्ण काम (जैसे user input पर respond करना) से interrupt हो सकती हैं:
import { useState, useTransition } from "react";
function SearchableList({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
// ये update urgent है — input को keystroke तुरंत reflect करना चाहिए
setQuery(value);
// ये update urgent नहीं है — 10,000 items filter करना इंतजार कर सकता है
startTransition(() => {
const filtered = items.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(filtered);
});
}
return (
<div>
<input
type="text"
value={query}
onChange={handleSearch}
placeholder="Search..."
className="w-full rounded border px-4 py-2"
/>
{isPending && (
<p className="mt-2 text-sm text-gray-500">Filtering...</p>
)}
<ul className="mt-4 space-y-2">
{filteredItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}startTransition के बिना, search input में type करना sluggish लगेगा क्योंकि React DOM update करने से पहले 10,000 items synchronously filter करने की कोशिश करेगा। startTransition के साथ, input तुरंत update होता है, और filtering background में होती है।
मैंने एक tool page पर INP मापी जिसमें complex input handler था। useTransition से पहले: 380ms INP। बाद: 90ms INP। React API change से 76% सुधार।
Handlers जो महंगे operations trigger करते हैं (API calls, heavy computation), उन्हें debounce करें:
import { useCallback, useRef } from "react";
function useDebounce<T extends (...args: unknown[]) => void>(
fn: T,
delay: number
): T {
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
return useCallback(
((...args: unknown[]) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => fn(...args), delay);
}) as T,
[fn, delay]
);
}
// Usage
function LiveSearch() {
const [results, setResults] = useState<SearchResult[]>([]);
const search = useDebounce(async (query: string) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
setResults(data.results);
}, 300);
return (
<input
type="text"
onChange={(e) => search(e.target.value)}
placeholder="Search..."
/>
);
}300ms मेरा go-to debounce value है। इतना छोटा कि users delay notice नहीं करते, इतना लंबा कि हर keystroke पर fire होने से बचाता है।
अगर आपके पास वाकई भारी computation है (बड़ा JSON parse करना, image manipulation, complex calculations), तो इसे main thread से पूरी तरह हटा दें:
// worker.ts
self.addEventListener("message", (event) => {
const { data, operation } = event.data;
switch (operation) {
case "sort": {
// बड़े datasets के लिए 500ms ले सकता है
const sorted = data.sort((a: number, b: number) => a - b);
self.postMessage({ result: sorted });
break;
}
case "filter": {
const filtered = data.filter((item: DataItem) =>
complexFilterLogic(item)
);
self.postMessage({ result: filtered });
break;
}
}
});// useWorker.ts
import { useEffect, useRef, useCallback } from "react";
function useWorker() {
const workerRef = useRef<Worker>();
useEffect(() => {
workerRef.current = new Worker(
new URL("../workers/worker.ts", import.meta.url)
);
return () => workerRef.current?.terminate();
}, []);
const process = useCallback(
(operation: string, data: unknown): Promise<unknown> => {
return new Promise((resolve) => {
if (!workerRef.current) return;
workerRef.current.onmessage = (event) => {
resolve(event.data.result);
};
workerRef.current.postMessage({ operation, data });
});
},
[]
);
return { process };
}Web Workers एक अलग thread पर operate करते हैं, इसलिए 2-second computation भी INP को बिल्कुल affect नहीं करेगी। Main thread user interactions handle करने के लिए free रहता है।
मेरे INP results:
अगर आप Next.js (13+ App Router के साथ) पर हैं, तो आपके पास कुछ powerful performance primitives हैं जिनका ज्यादातर developers पूरा फायदा नहीं उठाते।
next/image बढ़िया है, लेकिन default configuration performance की बहुत गुंजाइश छोड़ती है:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
minimumCacheTTL: 60 * 60 * 24 * 365, // 1 साल
},
};
export default nextConfig;मुख्य settings:
formats: ["image/avif", "image/webp"] — AVIF, WebP से 20-50% छोटा है। क्रम मायने रखता है: Next.js पहले AVIF try करता है, WebP पर fallback करता है, फिर original format पर।minimumCacheTTL — Default 60 seconds है। Blog के लिए, images बदलती नहीं। एक साल के लिए cache करें।deviceSizes और imageSizes — Defaults में 3840px शामिल है। जब तक आप 4K images serve नहीं कर रहे, इस list को trim करें। हर size एक अलग cached image generate करता है, और unused sizes disk space और build time waste करते हैं।और हमेशा sizes prop use करें ताकि browser को पता चले image किस size पर render होगी:
// Full-width hero image
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
sizes="100vw"
priority // LCP image — इसे lazy load मत करें!
/>
// Responsive grid में Card image
<Image
src="/card.jpg"
alt="Card"
width={400}
height={300}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>sizes के बिना, browser 300px slot के लिए 1200px image download कर सकता है। ये wasted bytes और wasted time है।
LCP image पर priority prop critical है। ये lazy loading disable करता है और automatically fetchPriority="high" जोड़ता है। अगर आपका LCP element next/image है, तो बस priority जोड़ें और आप ज्यादातर रास्ता तय कर लेंगे।
मैंने इसे CLS section में cover किया, लेकिन ये emphasis deserve करता है। next/font एकमात्र font loading solution है जिसे मैंने consistently zero CLS achieve करते देखा है:
import { Inter, JetBrains_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-mono",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html
lang="en"
className={`${inter.variable} ${jetbrainsMono.variable}`}
>
<body className="font-sans">{children}</body>
</html>
);
}दो fonts, zero CLS, runtime पर zero external requests। Fonts build time पर download होते हैं और आपके अपने domain से serve होते हैं।
यहीं Next.js performance के लिए वाकई interesting हो जाता है। App Router के साथ, आप page के हिस्से browser को तैयार होते ही stream कर सकते हैं:
import { Suspense } from "react";
import { PostList } from "@/components/blog/PostList";
import { Sidebar } from "@/components/blog/Sidebar";
import { PostListSkeleton } from "@/components/blog/PostListSkeleton";
import { SidebarSkeleton } from "@/components/blog/SidebarSkeleton";
export default function BlogPage() {
return (
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
<div className="lg:col-span-2">
{/* ये तेज load होता है — तुरंत stream करें */}
<h1 className="text-4xl font-bold">Blog</h1>
{/* इसमें database query चाहिए — तैयार होने पर stream करें */}
<Suspense fallback={<PostListSkeleton />}>
<PostList />
</Suspense>
</div>
<aside>
{/* Sidebar independently load हो सकता है */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</aside>
</div>
);
}Browser shell (heading, navigation, layout) तुरंत receive करता है। Post list और sidebar उनका data उपलब्ध होते ही stream होते हैं। User तेज initial load देखता है, और content progressively भरता है।
ये LCP के लिए विशेष रूप से powerful है। अगर आपका LCP element heading है (post list नहीं), तो ये database query कितनी भी देर लगे, तुरंत render होता है।
Next.js आपको route segment level पर caching और revalidation configure करने देता है:
// app/blog/page.tsx
// इस page को हर घंटे revalidate करें
export const revalidate = 3600;
// app/tools/[slug]/page.tsx
// ये tool pages पूरी तरह static हैं — build time पर generate करें
export const dynamic = "force-static";
// app/api/search/route.ts
// API route — कभी cache न करें
export const dynamic = "force-dynamic";इस site पर, blog posts revalidate = 3600 (1 घंटा) use करते हैं। Tool pages force-static use करते हैं क्योंकि उनका content deployments के बीच कभी बदलता नहीं। Search API force-dynamic use करता है क्योंकि हर request unique है।
नतीजा: ज्यादातर pages static cache से serve होते हैं, cached pages के लिए TTFB 50ms से नीचे है, और server पर बमुश्किल load आता है।
Performance की आपकी perception unreliable है। आपकी development machine में 32GB RAM, NVMe SSD, और gigabit connection है। आपके users में नहीं।
1. Chrome DevTools Performance Panel
सबसे detailed उपलब्ध tool। Page load record करें, flamechart देखें, long tasks identify करें, render-blocking resources ढूंढें। मैं अपना ज्यादातर debugging time यहां बिताता हूं।
देखने वाली मुख्य बातें:
2. Lighthouse
Quick check के लिए अच्छा, लेकिन Lighthouse scores के लिए optimize मत करें। Lighthouse simulated throttled environment में चलता है जो real-world conditions से perfectly match नहीं करता। मैंने pages देखे हैं जो Lighthouse में 98 score करते हैं और field में 4s LCP रखते हैं।
Directional guidance के लिए Lighthouse use करें, scoreboard के रूप में नहीं।
3. PageSpeed Insights
Production sites के लिए सबसे महत्वपूर्ण tool क्योंकि ये real CrUX data दिखाता है — पिछले 28 दिनों में real Chrome users से actual measurements। Lab data बताता है क्या हो सकता है। CrUX data बताता है क्या होता है।
4. web-vitals Library
Real user metrics collect करने के लिए इसे अपनी production site में जोड़ें:
// components/analytics/WebVitals.tsx
"use client";
import { useEffect } from "react";
import { onCLS, onINP, onLCP, onFCP, onTTFB } from "web-vitals";
import type { Metric } from "web-vitals";
function sendToAnalytics(metric: Metric) {
// अपने analytics endpoint को भेजें
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // "good" | "needs-improvement" | "poor"
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
});
// sendBeacon use करें ताकि page unload block न हो
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/vitals", body);
} else {
fetch("/api/vitals", {
body,
method: "POST",
keepalive: true,
});
}
}
export function WebVitals() {
useEffect(() => {
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}, []);
return null;
}ये आपको अपना CrUX-जैसा data देता है, लेकिन ज्यादा detail के साथ। आप page, device type, connection speed, geographic region — जो भी चाहिए उसके आधार पर segment कर सकते हैं।
5. Chrome User Experience Report (CrUX)
CrUX BigQuery dataset free है और इसमें लाखों origins के लिए 28-day rolling data है। अगर आपकी site को enough traffic मिलता है, तो आप अपना data query कर सकते हैं:
SELECT
origin,
p75_lcp,
p75_cls,
p75_inp,
form_factor
FROM
`chrome-ux-report.materialized.metrics_summary`
WHERE
origin = 'https://yoursite.com'
AND yyyymm = 202603Third-party scripts ज्यादातर websites पर नंबर एक performance killer हैं। यहां बताया गया है कि मुझे क्या मिला और मैंने क्या किया।
GTM खुद ~80KB है। लेकिन GTM और scripts load करता है — analytics, marketing pixels, A/B testing tools। मैंने GTM configurations देखे हैं जो 2MB total के 15 additional scripts load करते हैं।
मेरा approach: Production में GTM use न करें। Analytics scripts directly load करें, सब कुछ defer करें, और जो scripts इंतजार कर सकती हैं उनके लिए loading="lazy" use करें:
// GTM से सब कुछ load करने की बजाय
// सिर्फ वो load करें जो चाहिए, जब चाहिए
export function AnalyticsScript() {
return (
<script
defer
src="https://analytics.example.com/script.js"
data-website-id="your-id"
/>
);
}अगर आपको absolutely GTM use करना ही है, तो इसे page interactive होने के बाद load करें:
"use client";
import { useEffect } from "react";
export function DeferredGTM({ containerId }: { containerId: string }) {
useEffect(() => {
// Page load के बाद GTM inject करने का इंतजार करें
const timer = setTimeout(() => {
const script = document.createElement("script");
script.src = `https://www.googletagmanager.com/gtm.js?id=${containerId}`;
script.async = true;
document.head.appendChild(script);
}, 3000); // 3 second delay
return () => clearTimeout(timer);
}, [containerId]);
return null;
}हां, आप पहले 3 seconds में bounce करने वाले users का data खो देंगे। मेरे experience में, ये trade-off करने लायक है। वो users वैसे भी convert नहीं हो रहे थे।
Live chat widgets (Intercom, Drift, Crisp) सबसे बुरे offenders में से हैं। अकेले Intercom 400KB+ JavaScript load करता है। जिस page पर 2% users actually chat button click करते हैं, वहां ये 98% users के लिए 400KB JavaScript है।
मेरा solution: Interaction पर widget load करें।
"use client";
import { useState } from "react";
export function ChatButton() {
const [loaded, setLoaded] = useState(false);
function loadChat() {
if (loaded) return;
// Chat widget script सिर्फ तब load करें जब user click करे
const script = document.createElement("script");
script.src = "https://chat-widget.example.com/widget.js";
script.onload = () => {
// Script load होने के बाद widget initialize करें
window.ChatWidget?.open();
};
document.head.appendChild(script);
setLoaded(true);
}
return (
<button
onClick={loadChat}
className="fixed bottom-4 right-4 rounded-full bg-blue-600 p-4 text-white shadow-lg"
aria-label="Open chat"
>
{loaded ? "Loading..." : "Chat with us"}
</button>
);
}Chrome DevTools में Coverage चलाएं (Ctrl+Shift+P > "Show Coverage")। ये आपको बिल्कुल दिखाता है कि current page पर हर script का कितना actually use हुआ।
Typical Next.js site पर, मुझे आमतौर पर मिलता है:
Button import करते हैं, लेकिन पूरी library bundle हो जाती है। Solution: tree-shakeable libraries use करें या subpaths से import करें (import { Button } from "lib" की बजाय import Button from "lib/Button")।Promise, fetch, या Array.prototype.includes के polyfills ship कर रहे हैं? 2026 में, आपको इनकी जरूरत नहीं।मैं oversized chunks ढूंढने के लिए Next.js bundle analyzer use करता हूं:
// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";
const nextConfig = {
// your config
};
export default process.env.ANALYZE === "true"
? withBundleAnalyzer({ enabled: true })(nextConfig)
: nextConfig;ANALYZE=true npm run buildये आपके bundles का visual treemap खोलता है। मुझे 120KB date formatting library मिली जिसे native Intl.DateTimeFormat से replace किया। 90KB markdown parser मिला एक page पर import जो markdown use ही नहीं करता था। छोटी wins जो add up होती हैं।
मैंने इसे LCP section में mention किया, लेकिन दोहराने लायक है क्योंकि ये इतना common है। <head> में हर <link rel="stylesheet"> rendering block करता है। अगर आपके पास पांच stylesheets हैं, तो browser पांचों के लिए इंतजार करता है।
Tailwind के साथ Next.js इसे अच्छी तरह handle करता है — CSS inline और minimal है। लेकिन अगर आप third-party CSS import कर रहे हैं, तो audit करें:
// खराब — हर page पर पूरी library CSS load करता है
import "some-library/dist/styles.css";
// बेहतर — dynamic import ताकि सिर्फ जरूरत वाले pages पर load हो
const SomeComponent = dynamic(
() => import("some-library").then((mod) => {
// CSS dynamic component के अंदर import होती है
import("some-library/dist/styles.css");
return mod.SomeComponent;
}),
{ ssr: false }
);इस site के tools page की actual optimization से गुजरते हैं। ये 15+ interactive tools वाला page है, हर एक का अपना component है, और उनमें से कुछ (जैसे regex tester और JSON formatter) JavaScript-heavy हैं।
Initial measurements (CrUX data, mobile, 75th percentile):
Lighthouse score: 62।
LCP analysis: LCP element page heading (<h1>) था, जो तुरंत render होना चाहिए। लेकिन ये delay हो रहा था:
CLS analysis: तीन sources:
INP analysis: Regex tester tool सबसे बड़ा offender था। Regex input में हर keystroke trigger करता था:
Per keystroke total time: 280-400ms।
Week 1: LCP और CLS
Google Fonts CDN को next/font से replace किया। Font अब self-hosted है, build time पर loaded, size-adjusted fallback के साथ। Fonts से CLS: 0.06 → 0.00
Component library CSS हटाई। जो 3 components use कर रहा था उन्हें Tailwind से rewrite किया। Total CSS हटाई: 180KB। Render-blocking CSS: eliminated
Tools page और tool detail pages में revalidate = 3600 जोड़ा। पहली hit server-rendered, बाद की hits cache से serve। TTFB: 420ms → 45ms (cached)
सभी tool card components में explicit dimensions जोड़ीं और responsive layouts के लिए aspect-ratio use किया। Cards से CLS: 0.04 → 0.00
Cookie banner को screen के bottom पर position: fixed में move किया। Banner से CLS: 0.02 → 0.00
Week 2: INP
startTransition में wrap किया:function RegexTester() {
const [pattern, setPattern] = useState("");
const [testString, setTestString] = useState("");
const [results, setResults] = useState<RegexResult[]>([]);
const [isPending, startTransition] = useTransition();
function handlePatternChange(value: string) {
setPattern(value); // Urgent: input update करें
startTransition(() => {
// Non-urgent: matches compute करें
try {
const regex = new RegExp(value, "g");
const matches = [...testString.matchAll(regex)];
setResults(
matches.map((m) => ({
match: m[0],
index: m.index ?? 0,
groups: m.groups,
}))
);
} catch {
setResults([]);
}
});
}
return (
<div>
<input
value={pattern}
onChange={(e) => handlePatternChange(e.target.value)}
className={isPending ? "opacity-70" : ""}
/>
{/* results rendering */}
</div>
);
}Regex tester पर INP: 380ms → 85ms
JSON formatter के input handler में debouncing जोड़ी (300ms delay)। JSON formatter पर INP: 260ms → 60ms
Hash generator की computation को Web Worker में move किया। Large inputs की SHA-256 hashing अब main thread से पूरी तरह अलग होती है। Hash generator पर INP: 200ms → 40ms
दो हफ्तों की optimization के बाद (CrUX data, mobile, 75th percentile):
Lighthouse score: 62 → 97।
तीनों metrics मजबूती से "Good" range में। Page mobile पर instant feel होता है। और improvements के बाद के महीने में organic search traffic 12% बढ़ा (हालांकि मैं causation prove नहीं कर सकता — अन्य factors भी काम कर रहे थे)।
अगर आप इस post से कुछ और नहीं लेते, तो यहां वो checklist है जो मैं हर project पर चलाता हूं:
priority (या fetchPriority="high") जोड़ें<head> में LCP resources preload करेंnext/font से fonts self-host करेंwidth और heightnext/font use करनाposition: fixed/absolute या reserved space use करता हैstartTransition में wrappedPerformance optimization एक बार का काम नहीं है। ये एक discipline है। हर नया feature, हर नई dependency, हर नई third-party script potential regression है। जो sites तेज रहती हैं वो हैं जहां कोई metrics continuously देख रहा है, वो नहीं जहां किसी ने एक बार optimization sprint किया।
Real user monitoring set up करें। Metrics regress होने पर alerts set up करें। Performance को अपनी code review process का हिस्सा बनाएं। जब कोई 200KB library जोड़े, पूछें कि क्या 5KB alternative है। जब कोई event handler में synchronous computation जोड़े, पूछें कि क्या इसे defer या worker में move किया जा सकता है।
इस post की techniques theoretical नहीं हैं। ये मैंने वास्तव में किया, इस site पर, back up करने के लिए real numbers के साथ। आपका mileage अलग होगा — हर site अलग है, हर audience अलग है, हर infrastructure अलग है। लेकिन principles universal हैं: कम load करें, smart load करें, main thread block न करें।
आपके users तेज site के लिए thank-you note नहीं भेजेंगे। लेकिन वो रुकेंगे। वो वापस आएंगे। और Google notice करेगा।