Tornar al blog
Disseny i Desenvolupament Web

Core Web Vitals 2026: Optimització per a Next.js i React

Imatge destacada de l'article: core web vitals 2026 nextjs react

Core Web Vitals 2026: optimització per a Next.js i React

Els Core Web Vitals han evolucionat significativament des de la seva introducció el 2020. El 2026, amb la consolidació de l'INP (Interaction to Next Paint) com a mètrica oficial i les capacitats avançades de Next.js 15, optimitzar el rendiment web requereix un enfocament completament renovat. Aquesta guia tècnica et mostra exactament com dominar cada mètrica utilitzant les eines més modernes de l'ecosistema React.

Google ha confirmat que els llocs que superen els llindars de Core Web Vitals tenen un 24% menys d'abandonament i millors posicions en els resultats de cerca. Per a projectes desenvolupats amb React i Next.js, les oportunitats d'optimització són enormes, però també ho són els desafiaments tècnics.

Què són els Core Web Vitals el 2026 i per què importen?

Els Core Web Vitals són un conjunt de mètriques centrades en l'usuari que mesuren l'experiència real de càrrega, interactivitat i estabilitat visual d'una pàgina web. El 2026, les tres mètriques fonamentals són:

  • LCP (Largest Contentful Paint): Temps fins que l'element més gran del viewport és visible
  • INP (Interaction to Next Paint): Latència de totes les interaccions de l'usuari
  • CLS (Cumulative Layout Shift): Estabilitat visual durant tota la sessió

A diferència de mètriques sintètiques com Time to First Byte o Speed Index, els Core Web Vitals reflecteixen l'experiència percebuda per usuaris reals navegant en condicions reals de xarxa i dispositiu.

Llindars actualitzats per al 2026

El canvi més significatiu va ser la substitució de FID (First Input Delay) per INP al març de 2024. Mentre que FID només mesurava la primera interacció, INP avalua la latència de totes les interaccions durant la sessió, oferint una visió molt més realista de la interactivitat.

Com funciona LCP i com optimitzar-lo a Next.js 15?

LCP mesura el temps que triga a renderitzar-se l'element de contingut més gran visible al viewport. Típicament, aquest element és una imatge hero, un bloc de text prominent o un vídeo poster.

Elements que qualifiquen per a LCP

  • Elements (incloent dins de )
  • Imatges de fons carregades via url() en CSS
  • Elements amb poster
  • Elements de text (

    ,

    , etc.) amb contingut significatiu

  • Elements amb imatges de fons inline

Tècniques d'optimització LCP a Next.js 15

1. Ús correcte del component Image

Next.js 15 inclou un component Image optimitzat que implementa lazy loading intel·ligent, però per a l'element LCP necessitem el comportament oposat:

// app/page.tsx
import Image from 'next/image';

export default function HomePage() {
  return (
    <main>
      <section className="hero">
        {/* Imatge LCP: priority + fetchPriority */}
        <Image
          src="/hero-banner.webp"
          alt="Descripció del hero"
          width={1920}
          height={1080}
          priority // Desactiva lazy loading
          fetchPriority="high" // Hint al navegador
          sizes="100vw"
          quality={85}
        />
      </section>
      
      {/* Resta d'imatges: lazy loading per defecte */}
      <Image
        src="/secondary-image.webp"
        alt="Imatge secundària"
        width={800}
        height={600}
        loading="lazy"
      />
    </main>
  );
}

2. Preload de recursos crítics

En el App Router de Next.js 15, pots gestionar preloads directament en el layout:

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'El Meu Lloc Web',
  other: {
    'link': [
      {
        rel: 'preload',
        href: '/fonts/inter-var.woff2',
        as: 'font',
        type: 'font/woff2',
        crossOrigin: 'anonymous',
      },
    ],
  },
};

// O usant el head
export default function RootLayout({ children }) {
  return (
    <html lang="ca">
      <head>
        <link
          rel="preload"
          href="/hero-banner.webp"
          as="image"
          fetchPriority="high"
        />
        <link
          rel="preconnect"
          href="https://fonts.googleapis.com"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

3. Streaming i Suspense per a LCP

Una tècnica avançada és utilitzar Suspense per prioritzar el contingut LCP:

// app/page.tsx
import { Suspense } from 'react';
import HeroBanner from '@/components/HeroBanner';
import DynamicContent from '@/components/DynamicContent';

export default function Page() {
  return (
    <>
      {/* Contingut LCP renderitza immediatament */}
      <HeroBanner />
      
      {/* Contingut secundari en streaming */}
      <Suspense fallback={<ContentSkeleton />}>
        <DynamicContent />
      </Suspense>
    </>
  );
}

Què és INP i per què va reemplaçar FID?

INP (Interaction to Next Paint) mesura la latència entre una interacció de l'usuari i el següent frame visual actualitzat. A diferència de FID, que només considerava la primera interacció, INP avalua totes les interaccions i reporta un valor proper al pitjor cas.

Anatomia d'una interacció

Cada interacció té tres fases:

  1. Input delay: Temps des de l'esdeveniment fins que comença el handler
  2. Processing time: Temps executant els event handlers
  3. Presentation delay: Temps fins que el navegador renderitza el frame
[Click] → [Input Delay] → [Processing] → [Presentation] → [Visual Update]
         └─────────────── INP Total ───────────────────┘

Interaccions que compta INP

  • Clics del ratolí
  • Taps en pantalles tàctils
  • Pulsacions de tecles (tant físiques com de teclat en pantalla)

No compta: Hover, scroll o zoom (considerats interaccions contínues).

Optimització INP en React i Next.js

1. Evitar bloquejos del main thread

El problema més comú de INP en React són renders síncrons extensos:

// ❌ MAL: Re-render costós bloqueja interaccions
function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  
  // Filtrat síncron en cada render
  const filtered = products.filter(p => 
    p.name.toLowerCase().includes(filter.toLowerCase())
  );
  
  return (
    <>
      <input 
        value={filter} 
        onChange={e => setFilter(e.target.value)} 
      />
      {filtered.map(p => <ProductCard key={p.id} product={p} />)}
    </>
  );
}

// ✅ BÉ: useDeferredValue per prioritzar input
import { useDeferredValue, useMemo } from 'react';

function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  const deferredFilter = useDeferredValue(filter);
  
  const filtered = useMemo(() => 
    products.filter(p => 
      p.name.toLowerCase().includes(deferredFilter.toLowerCase())
    ),
    [products, deferredFilter]
  );
  
  const isStale = filter !== deferredFilter;
  
  return (
    <>
      <input 
        value={filter} 
        onChange={e => setFilter(e.target.value)} 
      />
      <div style={{ opacity: isStale ? 0.7 : 1 }}>
        {filtered.map(p => <ProductCard key={p.id} product={p} />)}
      </div>
    </>
  );
}

2. useTransition per a actualitzacions no urgents

import { useState, useTransition } from 'react';

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();
  
  function handleTabChange(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  
  return (
    <div>
      <nav>
        {['home', 'products', 'about'].map(t => (
          <button
            key={t}
            onClick={() => handleTabChange(t)}
            className={tab === t ? 'active' : ''}
          >
            {t}
          </button>
        ))}
      </nav>
      
      <div style={{ opacity: isPending ? 0.8 : 1 }}>
        <TabContent tab={tab} />
      </div>
    </div>
  );
}

3. Virtualització per a llistes llargues

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualizedList({ items }) {
  const parentRef = useRef(null);
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80,
    overscan: 5,
  });
  
  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map(virtualItem => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${virtualItem.start}px)`,
              height: `${virtualItem.size}px`,
            }}
          >
            <ItemCard item={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Com prevenir CLS en aplicacions React modernes?

CLS (Cumulative Layout Shift) mesura els canvis inesperats en el layout durant tota la vida de la pàgina. Un CLS alt frustra els usuaris que intenten interactuar amb elements que es mouen.

Causes comunes de CLS

  1. Imatges sense dimensions: El navegador no reserva espai fins que descarrega la imatge
  2. Anuncis i embeds dinàmics: Contingut injectat que desplaça el layout
  3. Fonts web (FOIT/FOUT): Canvis de mida al carregar fonts
  4. Contingut inserit dinàmicament: Banners, notificacions, modals

Solucions específiques per a Next.js 15

1. Aspect ratio per a media

// Component d'imatge amb aspect ratio garantit
function ResponsiveImage({ src, alt, aspectRatio = '16/9' }) {
  return (
    <div style={{ aspectRatio, position: 'relative' }}>
      <Image
        src={src}
        alt={alt}
        fill
        style={{ objectFit: 'cover' }}
        sizes="(max-width: 768px) 100vw, 50vw"
      />
    </div>
  );
}

2. Skeleton loaders amb dimensions fixes

// components/ProductSkeleton.tsx
export function ProductSkeleton() {
  return (
    <div className="product-card" style={{ height: '320px' }}>
      <div 
        className="skeleton" 
        style={{ height: '200px', background: '#e0e0e0' }} 
      />
      <div 
        className="skeleton" 
        style={{ height: '24px', marginTop: '16px', width: '80%' }} 
      />
      <div 
        className="skeleton" 
        style={{ height: '20px', marginTop: '8px', width: '40%' }} 
      />
    </div>
  );
}

// Ús amb Suspense
import { Suspense } from 'react';

export default function ProductPage() {
  return (
    <Suspense fallback={<ProductSkeleton />}>
      <ProductDetails />
    </Suspense>
  );
}

3. Optimització de fonts a Next.js 15

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Evita FOIT
  variable: '--font-inter',
  preload: true,
  fallback: ['system-ui', 'sans-serif'],
  adjustFontFallback: true, // Ajusta mètriques del fallback
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-mono',
});

export default function RootLayout({ children }) {
  return (
    <html lang="ca" className={`${inter.variable} ${robotoMono.variable}`}>
      <body className={inter.className}>
        {children}
      </body>
    </html>
  );
}

4. Reservar espai per a contingut dinàmic

// Component de banner amb alçada reservada
function PromoBanner() {
  const [isVisible, setIsVisible] = useState(true);
  
  if (!isVisible) {
    // Mantenir l'espai o col·lapsar suaument
    return null;
  }
  
  return (
    <div 
      className="promo-banner"
      style={{ 
        minHeight: '60px', // Alçada reservada
        containIntrinsicSize: '0 60px', // content-visibility optimization
      }}
    >
      <p>Oferta especial! 20% de descompte</p>
      <button onClick={() => setIsVisible(false)}>×</button>
    </div>
  );
}

Com impacten els React Server Components en Core Web Vitals?

Els React Server Components (RSC), completament integrats en Next.js 15 App Router, transformen fonamentalment com optimitzem Core Web Vitals. Al executar-se exclusivament en el servidor, redueixen dràsticament el JavaScript enviat al client.

Beneficis de RSC per a cada mètrica

LCP: Renderitzat instantani

// app/products/[id]/page.tsx
// Aquest component és un Server Component per defecte

import { getProduct } from '@/lib/api';
import ProductImage from '@/components/ProductImage';

export default async function ProductPage({ params }) {
  // Fetch en el servidor - sense waterfall en client
  const product = await getProduct(params.id);
  
  return (
    <article>
      {/* HTML complet enviat en la resposta inicial */}
      <ProductImage 
        src={product.image} 
        alt={product.name}
        priority 
      />
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      
      {/* Només aquest component necessita JavaScript */}
      <AddToCartButton productId={product.id} />
    </article>
  );
}

INP: Menys JavaScript = menys bloquejos

// Comparació de bundles

// ❌ Enfocament tradicional (Client Component complet)
// Bundle: ~150KB de JavaScript
'use client';
import { useState, useEffect } from 'react';
import { formatPrice, calculateDiscount } from '@/lib/utils';
import { fetchReviews } from '@/lib/api';

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  const [reviews, setReviews] = useState([]);
  
  useEffect(() => {
    // Fetches en cascada en el client
  }, [productId]);
  
  // Tot el codi de formatació s'envia al client
}

// ✅ Enfocament RSC híbrid
// Bundle: ~15KB de JavaScript (només interactivitat)

// Server Component - 0KB al client
async function ProductPage({ params }) {
  const [product, reviews] = await Promise.all([
    getProduct(params.id),
    getReviews(params.id),
  ]);
  
  // Formatació executada en servidor
  const formattedPrice = formatPrice(product.price);
  const discount = calculateDiscount(product);
  
  return (
    <article>
      <h1>{product.name}</h1>
      <p className="price">{formattedPrice}</p>
      
      {/* Només components interactius són Client Components */}
      <InteractiveGallery images={product.images} />
      <AddToCartButton product={product} />
      
      {/* Reviews renderitzades en servidor */}
      <ReviewList reviews={reviews} />
    </article>
  );
}

Patró de composició òptim

// app/dashboard/page.tsx
import { Suspense } from 'react';
import DashboardHeader from '@/components/DashboardHeader';
import StatsCards from '@/components/StatsCards';
import RecentActivity from '@/components/RecentActivity';
import InteractiveChart from '@/components/InteractiveChart';

export default function DashboardPage() {
  return (
    <div className="dashboard">
      {/* Server Component - renderitzat instantani */}
      <DashboardHeader />
      
      {/* Server Component amb dades - streaming */}
      <Suspense fallback={<StatsCardsSkeleton />}>
        <StatsCards />
      </Suspense>
      
      {/* Client Component - càrrega diferida */}
      <Suspense fallback={<ChartSkeleton />}>
        <InteractiveChart />
      </Suspense>
      
      {/* Server Component - streaming paral·lel */}
      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

Quines eines utilitzar per mesurar Core Web Vitals?

La mesura precisa requereix combinar dades de laboratori (sintètiques) amb dades de camp (usuaris reals).

Eines de laboratori

Lighthouse en Next.js

# Instal·lar Lighthouse CI
npm install -g @lhci/cli

# Configuració: lighthouserc.js
module.exports = {
  ci: {
    collect: {
      startServerCommand: 'npm run start',
      url: ['http://localhost:3000/', 'http://localhost:3000/productes'],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'interactive': ['error', { maxNumericValue: 3800 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
};

# Executar en CI/CD
lhci autorun

Chrome DevTools Performance

  1. Obre DevTools → Performance
  2. Activa "Web Vitals" en el panell
  3. Habilita "CPU throttling: 4x slowdown"
  4. Habilita "Network: Slow 3G"
  5. Grava una sessió d'usuari

Eines de camp (RUM)

web-vitals library

// lib/analytics.ts
import { onLCP, onINP, onCLS, Metric } from 'web-vitals';

function sendToAnalytics(metric: Metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  });
  
  // Utilitzar sendBeacon per no bloquejar unload
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

export function initWebVitals() {
  onLCP(sendToAnalytics);
  onINP(sendToAnalytics);
  onCLS(sendToAnalytics);
}
// app/layout.tsx
'use client';
import { useEffect } from 'react';
import { initWebVitals } from '@/lib/analytics';

export function WebVitalsReporter() {
  useEffect(() => {
    initWebVitals();
  }, []);
  
  return null;
}

Google Search Console i CrUX

El Chrome User Experience Report (CrUX) proporciona dades reals de milions d'usuaris Chrome. Accedeix a través de:

  • Search Console: Core Web Vitals report per URL/grup
  • PageSpeed Insights: Dades CrUX + diagnòstic Lighthouse
  • CrUX Dashboard: Visualitzacions en Data Studio
  • BigQuery: Consultes SQL sobre el dataset públic
-- Consulta CrUX en BigQuery
SELECT
  origin,
  p75_lcp,
  p75_inp,
  p75_cls
FROM
  `chrome-ux-report.materialized.metrics_summary`
WHERE
  origin = 'https://tudomini.com'
  AND yyyymm = 202601

Next.js Analytics (Vercel)

// next.config.js
module.exports = {
  experimental: {
    webVitalsAttribution: ['CLS', 'LCP', 'INP'],
  },
};

// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <SpeedInsights />
      </body>
    </html>
  );
}

Quins són els benchmarks de Core Web Vitals per tipus de lloc?

Els llindars universals de Google no expliquen tota la història. Diferents tipus de llocs tenen desafiaments únics i expectatives diferents.

E-commerce

Desafiaments específics:

  • Carrusels de productes (CLS)
  • Filtres dinàmics (INP)
  • Imatges d'alta qualitat (LCP)

SaaS / Dashboards

Desafiaments específics:

  • Gràfics interactius complexos (INP)
  • Actualitzacions en temps real (CLS)
  • Taules amb moltes dades (INP, LCP)

Blogs / Llocs de contingut

Desafiaments específics:

  • Imatges hero grans (LCP)
  • Anuncis i embeds (CLS)
  • Lazy loading de contingut (CLS)

Landing pages

Desafiaments específics:

  • Animacions d'entrada (CLS)
  • Vídeos autoplay (LCP)
  • CTAs interactius (INP)

Com implementar un pipeline de monitoratge continu?

L'optimització de Core Web Vitals no és un esforç únic. Requereix monitoratge continu i alertes primerenques.

GitHub Actions per a CI/CD

# .github/workflows/lighthouse.yml
name: Lighthouse CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Run Lighthouse
        uses: treosh/lighthouse-ci-action@v11
        with:
          configPath: './lighthouserc.js'
          uploadArtifacts: true
          temporaryPublicStorage: true
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
      
      - name: Comment PR with results
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(
              fs.readFileSync('.lighthouseci/manifest.json', 'utf8')
            );
            // Generar comentari amb mètriques

Dashboard de monitoratge personalitzat

Per a projectes enterprise, el nostre equip de desenvolupament Node.js implementa dashboards de monitoratge amb:

  • Alertes Slack/Email quan mètriques degraden
  • Comparatives històriques per release
  • Segmentació per tipus de pàgina i dispositiu
  • Correlació amb mètriques de negoci

Conclusió: El rendiment com a avantatge competitiu

Optimitzar Core Web Vitals el 2026 requereix entendre profundament les mètriques, dominar les eines de React i Next.js, i mantenir un procés de monitoratge continu. Els llocs que inverteixen en rendiment veuen millores tangibles en SEO, conversions i satisfacció de l'usuari.

Les tècniques cobertes en aquest article, des de React Server Components fins a virtualització i optimització INP, representen l'estat de l'art en desenvolupament web performant. Implementar-les correctament requereix experiència en arquitectura frontend i comprensió del comportament real dels usuaris.

El teu lloc Next.js necessita optimització de Core Web Vitals? A KIWOP som especialistes en desenvolupament React i optimització de rendiment. Contacta amb el nostre equip per a una auditoria gratuïta del teu lloc.

Preguntes freqüents sobre Core Web Vitals

Quina és la diferència entre INP i FID?

FID (First Input Delay) només mesurava la latència de la primera interacció de l'usuari amb la pàgina. INP (Interaction to Next Paint) avalua totes les interaccions durant tota la sessió i reporta un valor representatiu del pitjor cas (percentil 75). Això significa que INP és una mètrica molt més exigent i realista de la interactivitat percebuda.

Els Core Web Vitals afecten directament el rànquing a Google?

Sí, els Core Web Vitals són un factor de rànquing confirmat des de 2021. No obstant això, Google ha clarificat que el contingut rellevant i de qualitat continua sent més important. Els Core Web Vitals actuen com a "desempat" entre pàgines amb contingut similar, i com a barrera d'entrada per a posicions premium.

Com afecten els React Server Components al LCP?

Els React Server Components milloren significativament el LCP perquè l'HTML es genera completament en el servidor i s'envia en la resposta inicial. No hi ha waterfall de JavaScript → fetch → render en el client. El contingut LCP està present des del primer byte d'HTML.

Què és un bon score de INP per a un e-commerce?

Per a e-commerce competitiu, recomanem un INP inferior a 150ms (el llindar "bo" de Google és 200ms). Els top performers aconsegueixen menys de 100ms. Les àrees crítiques són filtres de productes, carrusels i processos de checkout.

El CLS es mesura durant tota la sessió o només en la càrrega inicial?

CLS es mesura durant tota la vida de la pàgina, però Google utilitza "session windows" per al càlcul final. Un session window és un grup de shifts amb menys d'1 segon entre ells i màxim 5 segons de durada. El CLS reportat és el màxim de tots els session windows.

Next.js 15 té optimitzacions automàtiques per a Core Web Vitals?

Sí. Next.js 15 inclou: optimització automàtica d'imatges (WebP/AVIF, lazy loading, dimensions), optimització de fonts (preload, font-display), prefetching intel·ligent de rutes, i React Server Components per defecte que redueixen el JavaScript enviat al client.

Com puc mesurar Core Web Vitals d'usuaris reals, no només Lighthouse?

Utilitza la llibreria web-vitals de Google per capturar mètriques RUM (Real User Monitoring). Combina-la amb el teu stack d'analytics existent o serveis com Vercel Analytics, Datadog RUM, o New Relic Browser. Les dades de CrUX a Search Console també mostren mètriques d'usuaris Chrome reals.

Val la pena optimitzar per al percentil 75 o he d'apuntar al 95?

Google utilitza el percentil 75 (p75) per als seus llindars, el que significa que el 75% dels teus usuaris ha d'experimentar bons valors. No obstant això, per a llocs premium, optimitzar per a p90 o p95 garanteix una experiència consistent fins i tot per a usuaris en condicions adverses (connexions lentes, dispositius antics).

Auditoria
tècnica inicial.

IA, seguretat i rendiment. Diagnòstic i proposta tancada per fases.

NDA disponible
Resposta <24h
Proposta per fases

La teva primera reunió és amb un Arquitecte de Solucions, no amb un comercial.

Sol·licitar diagnòstic