सामग्री पर जाएं
·25 मिनट पढ़ने का समय

2026 में Core Web Vitals: क्या वाकई फर्क डालता है

Theory भूलिए — मैंने एक real Next.js production site पर LCP 2.5s के नीचे, CLS zero, और INP 200ms के नीचे लाने के लिए असल में क्या किया। Specific techniques, vague advice नहीं।

साझा करें:X / TwitterLinkedIn

मैंने इस 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 है लेकिन ये नहीं बताता कि क्या काटें।

यहां बताया गया है कि मैंने वास्तव में क्या किया, उस क्रम में जो मायने रखता था।

2026 में Core Web Vitals अभी भी क्यों मायने रखते हैं#

सीधे बात करता हूं: 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 पिछले पांच सालों में ज्यादा नहीं बदला:

  • 53% mobile visits छोड़ दी जाती हैं अगर page load होने में 3 seconds से ज्यादा लगते हैं (Google/SOASTA research, अभी भी लागू)
  • हर 100ms latency conversions में लगभग 1% की लागत लगाती है (Amazon का original finding, बार-बार validated)
  • Layout shifts अनुभव करने वाले users काफी कम संभावना रखते हैं purchase पूरा करने या form भरने की

2026 में Core Web Vitals तीन metrics से बने हैं:

Metricक्या मापता हैअच्छासुधार चाहिएखराब
LCPLoading performance≤ 2.5s2.5s – 4.0s> 4.0s
CLSVisual stability≤ 0.10.1 – 0.25> 0.25
INPResponsiveness≤ 200ms200ms – 500ms> 500ms

ये thresholds मार्च 2024 में INP ने FID की जगह लेने के बाद से नहीं बदले हैं। लेकिन इन्हें हासिल करने की techniques विकसित हुई हैं, खासकर React/Next.js ecosystem में।

LCP: वो जो सबसे ज्यादा मायने रखता है#

Largest Contentful Paint मापता है कि viewport में सबसे बड़ा visible element कब rendering पूरी करता है। ज्यादातर pages के लिए, ये hero image, heading, या text का बड़ा block होता है।

Step 1: अपना असली LCP Element पहचानें#

कुछ भी 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 कर सकते हैं:

tsx
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।

Step 2: LCP Resource Preload करें#

अगर आपका 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 जोड़ सकते हैं:

tsx
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 से मिली सबसे बड़ी जीत है।

Step 3: Render-Blocking Resources हटाएं#

CSS rendering block करती है। सारी। अगर आपके पास <link rel="stylesheet"> से loaded 200KB stylesheet है, तो browser पूरी download और parse होने तक कुछ भी paint नहीं करेगा।

Fix तीन तरफा है:

  1. Critical CSS inline करें — Above-the-fold content के लिए जरूरी CSS extract करें और <head> में <style> tag में inline करें। Next.js ये automatically करता है जब आप CSS Modules या proper purging के साथ Tailwind use करते हैं।

  2. Non-critical CSS defer करें — अगर आपके पास below-the-fold content (footer animation library, chart component) के लिए stylesheets हैं, तो इन्हें asynchronously load करें:

html
<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>
  1. Unused CSS हटाएं — Tailwind CSS v4 अपने JIT engine से ये automatically करता है। लेकिन अगर आप third-party CSS libraries import कर रहे हैं, तो उनका audit करें। मुझे एक component library मिली जो एक tooltip component के लिए 180KB CSS import कर रही थी। इसे 20-line custom component से replace किया और 170KB बचाए।

Step 4: Server Response Time (TTFB)#

LCP तेज नहीं हो सकता अगर TTFB धीमा है। अगर आपका server respond करने में 800ms लेता है, तो आपका LCP कम से कम 800ms + बाकी सब कुछ होगा।

इस site पर (Node.js + PM2 + Nginx एक VPS पर), मैंने cold hit पर TTFB लगभग 180ms मापा। यहां बताया गया है कि मैंने इसे वहीं रखने के लिए क्या किया:

  • Blog posts के लिए ISR (Incremental Static Regeneration) — pages build time पर pre-rendered होते हैं और periodically revalidate होते हैं। पहली visit Nginx के reverse proxy cache से directly static file serve करती है।
  • Edge caching headers — static pages पर Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
  • Nginx में Gzip/Brotli compression — transfer size 60-80% कम करता है।
bash
# 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 पर मेरा पहले/बाद:

  • Optimization से पहले: 3.8s (75th percentile, CrUX)
  • Preload + fetchPriority + compression के बाद: 1.8s
  • कुल सुधार: 53% कमी

CLS: हजार Shifts की मौत#

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 करें।

tsx
// खराब — 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 करेगी:

tsx
// खराब — 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:

tsx
// 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 करें

tsx
// 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 करें:

tsx
// बिना 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 दिखाएं:

tsx
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:

  • पहले: 0.12 (सिर्फ font swap 0.06 था)
  • बाद: 0.00 — literally zero, CrUX data में हजारों page loads में

INP: नया बच्चा जो काटता है#

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 होता है।

उच्च INP का कारण क्या है#

  1. Main thread पर Long tasks — कोई भी JavaScript execution जो 50ms से ज्यादा लेती है main thread block करती है। Long task के दौरान होने वाली user interactions को इंतजार करना पड़ता है।
  2. React में महंगे re-renders — एक state update जो 200 components को re-render करवाती है, समय लेती है। User कुछ click करता है, React reconcile करता है, और paint 300ms तक नहीं होता।
  3. Layout thrashing — Layout properties (जैसे offsetHeight) read करना फिर उन्हें (जैसे style.height बदलना) loop में write करना browser को synchronously layout recalculate करने पर मजबूर करता है।
  4. बड़ा DOM — ज्यादा DOM nodes का मतलब धीमी style recalculation और layout। 5,000 nodes वाला DOM 500 nodes वाले से noticeably धीमा है।

scheduler.yield() से Long Tasks तोड़ना#

INP के लिए सबसे प्रभावशाली technique long tasks तोड़ना है ताकि browser chunks के बीच user interactions process कर सके:

tsx
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 कर सकते हैं:

tsx
function yieldToMain(): Promise<void> {
  if ("scheduler" in globalThis && "yield" in scheduler) {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

Non-Urgent Updates के लिए useTransition#

React 18+ हमें useTransition देता है, जो React को बताता है कि कुछ state updates urgent नहीं हैं और ज्यादा महत्वपूर्ण काम (जैसे user input पर respond करना) से interrupt हो सकती हैं:

tsx
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% सुधार।

Input Handlers को Debounce करना#

Handlers जो महंगे operations trigger करते हैं (API calls, heavy computation), उन्हें debounce करें:

tsx
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 के लिए Web Workers#

अगर आपके पास वाकई भारी computation है (बड़ा JSON parse करना, image manipulation, complex calculations), तो इसे main thread से पूरी तरह हटा दें:

tsx
// 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;
    }
  }
});
tsx
// 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:

  • पहले: 340ms (worst interaction complex input handling वाला regex tester tool था)
  • useTransition + debouncing के बाद: 110ms
  • सुधार: 68% कमी

Next.js Specific Wins#

अगर आप Next.js (13+ App Router के साथ) पर हैं, तो आपके पास कुछ powerful performance primitives हैं जिनका ज्यादातर developers पूरा फायदा नहीं उठाते।

next/image — लेकिन ठीक से Configured#

next/image बढ़िया है, लेकिन default configuration performance की बहुत गुंजाइश छोड़ती है:

tsx
// 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 होगी:

tsx
// 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 जोड़ें और आप ज्यादातर रास्ता तय कर लेंगे।

next/font — Zero Layout Shift Fonts#

मैंने इसे CLS section में cover किया, लेकिन ये emphasis deserve करता है। next/font एकमात्र font loading solution है जिसे मैंने consistently zero CLS achieve करते देखा है:

tsx
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 होते हैं।

Suspense के साथ Streaming#

यहीं Next.js performance के लिए वाकई interesting हो जाता है। App Router के साथ, आप page के हिस्से browser को तैयार होते ही stream कर सकते हैं:

tsx
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 होता है।

Route Segment Configuration#

Next.js आपको route segment level पर caching और revalidation configure करने देता है:

tsx
// 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 आता है।

Measurement Tools: Data पर भरोसा करें, अपनी आंखों पर नहीं#

Performance की आपकी perception unreliable है। आपकी development machine में 32GB RAM, NVMe SSD, और gigabit connection है। आपके users में नहीं।

मेरा Measurement Stack#

1. Chrome DevTools Performance Panel

सबसे detailed उपलब्ध tool। Page load record करें, flamechart देखें, long tasks identify करें, render-blocking resources ढूंढें। मैं अपना ज्यादातर debugging time यहां बिताता हूं।

देखने वाली मुख्य बातें:

  • Tasks पर red corners = long tasks (>50ms)
  • JavaScript द्वारा trigger किए गए Layout/Paint events
  • बड़े "Evaluate Script" blocks (बहुत ज्यादा JavaScript)
  • Late-discovered resources दिखाने वाला Network waterfall

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 में जोड़ें:

tsx
// 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 कर सकते हैं:

sql
SELECT
  origin,
  p75_lcp,
  p75_cls,
  p75_inp,
  form_factor
FROM
  `chrome-ux-report.materialized.metrics_summary`
WHERE
  origin = 'https://yoursite.com'
  AND yyyymm = 202603

Waterfall Killlist#

Third-party scripts ज्यादातर websites पर नंबर एक performance killer हैं। यहां बताया गया है कि मुझे क्या मिला और मैंने क्या किया।

Google Tag Manager (GTM)#

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 करें:

tsx
// 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 करें:

tsx
"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 नहीं हो रहे थे।

Chat Widgets#

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 करें।

tsx
"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>
  );
}

Unused JavaScript#

Chrome DevTools में Coverage चलाएं (Ctrl+Shift+P > "Show Coverage")। ये आपको बिल्कुल दिखाता है कि current page पर हर script का कितना actually use हुआ।

Typical Next.js site पर, मुझे आमतौर पर मिलता है:

  • Component libraries पूरी load होती हैं — आप UI library से Button import करते हैं, लेकिन पूरी library bundle हो जाती है। Solution: tree-shakeable libraries use करें या subpaths से import करें (import { Button } from "lib" की बजाय import Button from "lib/Button")।
  • Modern browsers के लिए Polyfills — Check करें कि आप Promise, fetch, या Array.prototype.includes के polyfills ship कर रहे हैं? 2026 में, आपको इनकी जरूरत नहीं।
  • Dead feature flags — Feature flags के पीछे code paths जो छह महीने से "on" हैं। Flag और dead branch हटा दें।

मैं oversized chunks ढूंढने के लिए Next.js bundle analyzer use करता हूं:

tsx
// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";
 
const nextConfig = {
  // your config
};
 
export default process.env.ANALYZE === "true"
  ? withBundleAnalyzer({ enabled: true })(nextConfig)
  : nextConfig;
bash
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 होती हैं।

Render-Blocking CSS#

मैंने इसे LCP section में mention किया, लेकिन दोहराने लायक है क्योंकि ये इतना common है। <head> में हर <link rel="stylesheet"> rendering block करता है। अगर आपके पास पांच stylesheets हैं, तो browser पांचों के लिए इंतजार करता है।

Tailwind के साथ Next.js इसे अच्छी तरह handle करता है — CSS inline और minimal है। लेकिन अगर आप third-party CSS import कर रहे हैं, तो audit करें:

tsx
// खराब — हर 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 }
);

एक Real Optimization Story#

इस site के tools page की actual optimization से गुजरते हैं। ये 15+ interactive tools वाला page है, हर एक का अपना component है, और उनमें से कुछ (जैसे regex tester और JSON formatter) JavaScript-heavy हैं।

Starting Point#

Initial measurements (CrUX data, mobile, 75th percentile):

  • LCP: 3.8s — खराब
  • CLS: 0.12 — सुधार चाहिए
  • INP: 340ms — खराब

Lighthouse score: 62।

Investigation#

LCP analysis: LCP element page heading (<h1>) था, जो तुरंत render होना चाहिए। लेकिन ये delay हो रहा था:

  1. Component library से 200KB CSS file (render-blocking)
  2. Google Fonts CDN से loaded custom font (धीमे connections पर 800ms FOIT)
  3. 420ms का TTFB क्योंकि page हर request पर बिना caching server-rendered था

CLS analysis: तीन sources:

  1. Google Fonts fallback से custom font में font swap: 0.06
  2. Height reservation बिना load होने वाले tool cards: 0.04
  3. Page के top पर inject होने वाला cookie banner, सब कुछ नीचे push करते हुए: 0.02

INP analysis: Regex tester tool सबसे बड़ा offender था। Regex input में हर keystroke trigger करता था:

  1. पूरे tool component का full re-render
  2. Test string के against regex evaluation
  3. Regex pattern की syntax highlighting

Per keystroke total time: 280-400ms।

Fixes#

Week 1: LCP और CLS

  1. Google Fonts CDN को next/font से replace किया। Font अब self-hosted है, build time पर loaded, size-adjusted fallback के साथ। Fonts से CLS: 0.06 → 0.00

  2. Component library CSS हटाई। जो 3 components use कर रहा था उन्हें Tailwind से rewrite किया। Total CSS हटाई: 180KB। Render-blocking CSS: eliminated

  3. Tools page और tool detail pages में revalidate = 3600 जोड़ा। पहली hit server-rendered, बाद की hits cache से serve। TTFB: 420ms → 45ms (cached)

  4. सभी tool card components में explicit dimensions जोड़ीं और responsive layouts के लिए aspect-ratio use किया। Cards से CLS: 0.04 → 0.00

  5. Cookie banner को screen के bottom पर position: fixed में move किया। Banner से CLS: 0.02 → 0.00

Week 2: INP

  1. Regex tester की result computation को startTransition में wrap किया:
tsx
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

  1. JSON formatter के input handler में debouncing जोड़ी (300ms delay)। JSON formatter पर INP: 260ms → 60ms

  2. Hash generator की computation को Web Worker में move किया। Large inputs की SHA-256 hashing अब main thread से पूरी तरह अलग होती है। Hash generator पर INP: 200ms → 40ms

Results#

दो हफ्तों की optimization के बाद (CrUX data, mobile, 75th percentile):

  • LCP: 3.8s → 1.8s (53% सुधार)
  • CLS: 0.12 → 0.00 (100% सुधार)
  • INP: 340ms → 110ms (68% सुधार)

Lighthouse score: 62 → 97।

तीनों metrics मजबूती से "Good" range में। Page mobile पर instant feel होता है। और improvements के बाद के महीने में organic search traffic 12% बढ़ा (हालांकि मैं causation prove नहीं कर सकता — अन्य factors भी काम कर रहे थे)।

Checklist#

अगर आप इस post से कुछ और नहीं लेते, तो यहां वो checklist है जो मैं हर project पर चलाता हूं:

LCP#

  • DevTools से LCP element identify करें
  • LCP image में priority (या fetchPriority="high") जोड़ें
  • <head> में LCP resources preload करें
  • Render-blocking CSS eliminate करें
  • next/font से fonts self-host करें
  • Brotli/Gzip compression enable करें
  • जहां possible हो static generation या ISR use करें
  • Static assets के लिए aggressive cache headers set करें

CLS#

  • सभी images में explicit width और height
  • Size-adjusted fallbacks के साथ next/font use करना
  • Dynamic content position: fixed/absolute या reserved space use करता है
  • Skeleton screens actual component dimensions से match करती हैं
  • Load के बाद top-of-page content injection नहीं

INP#

  • Interaction handlers के दौरान कोई long tasks (>50ms) नहीं
  • Non-urgent state updates startTransition में wrapped
  • Input handlers debounced (300ms)
  • Heavy computation Web Workers में offloaded
  • जहां possible हो DOM size 1,500 nodes से नीचे

General#

  • Page interactive होने के बाद Third-party scripts loaded
  • Bundle size analyzed और tree-shaken
  • Unused CSS हटाई
  • AVIF/WebP format में Images serve
  • Production में Real user monitoring (web-vitals library)

अंतिम विचार#

Performance 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 करेगा।

संबंधित पोस्ट