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:
- Input delay: Temps des de l'esdeveniment fins que comença el handler
- Processing time: Temps executant els event handlers
- 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
- Imatges sense dimensions: El navegador no reserva espai fins que descarrega la imatge
- Anuncis i embeds dinàmics: Contingut injectat que desplaça el layout
- Fonts web (FOIT/FOUT): Canvis de mida al carregar fonts
- 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 autorunChrome DevTools Performance
- Obre DevTools → Performance
- Activa "Web Vitals" en el panell
- Habilita "CPU throttling: 4x slowdown"
- Habilita "Network: Slow 3G"
- 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 = 202601Next.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ètriquesDashboard 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).