Nella seconda metà del 2026, la domanda centrale nelle decisioni di ingegneria frontend è questa: quale stato mantieni sul server e quale sul client? I React Server Components (RSC) sono usciti dalla beta nel 2023 e sono passati in produzione con Next.js 13 App Router. Vue 3.5 ha aggiunto il supporto per <script setup server> nella Composition API. Svelte 5 ha stabilizzato il sistema delle runes. Nel 2026, la domanda non è più "dovrei usare i server component?", ma piuttosto "cosa sposto sul server per ridurre il costo di hydration, e cosa tengo sul client per non danneggiare l'UX?". In questo articolo forniamo criteri pratici, risultati di benchmark e una mappa dei trade-off per tracciare quella linea.

L'Economia dell'Architettura Server-First: TBT e Trade-off di Bundle

La promessa fondamentale di un server component è questa: non inviare il bundle JavaScript al client, eseguire il rendering sul server e inviare lo stream HTML. Secondo il Chrome User Experience Report 2024, il Total Blocking Time (TBT) medio di un sito di e-commerce è 2190ms — gran parte proviene da React hydration. Con RSC, il TBT scende a 200-400ms perché al client arrivano solo le parti interattive (bottoni, form, slider).

Il trade-off è questo: ogni component che aumenta il rendering lato server contribuisce al TTFB (Time To First Byte). Se esegui il rendering di una card prodotto sul server, aggiungi +8-12ms al TTFB; se la renderizzi sul client, aggiungi +40-60ms al TBT. La decisione dipende da quale latenza l'utente percepisce meno. Su connessioni 3G, il costo del TTFB è elevato; su 5G, il costo del TBT è elevato.

C'è un secondo aspetto economico: la dimensione del bundle. Con RSC, solo il codice dei client component arriva al browser. Esempio: in un progetto Next.js 14, un chunk di 348KB è sceso a 89KB dopo la transizione a RSC (dato WebPageTest Dulles 3G Fast). Però ogni server component introduce il costo della serializzazione dei prop. Un array di prodotti parsato da JSON per 100 elementi occupa ~15KB di rete e 3ms di parse time — renderizzare gli stessi dati sul client richiedeva 8ms. Qui c'è un guadagno di 5ms, ma se non è nel critical path, non è significativo.

La Transizione di Vue 3.5: Markup dei Server nella Composition API

Vue 3.5 ha introdotto il blocco <script setup server> — trasferisce la logica di Nuxt 3 dalla directory server al single-file component. Questa struttura è ora valida:

<script setup server>
// Questo codice viene eseguito solo sul server
const products = await $fetch('/api/catalog', {
  headers: useRequestHeaders(['cookie'])
})
</script>

<script setup>
// Questo codice viene eseguito sia sul server che sul client
const selectedId = ref(null)
</script>

<template>
  <div v-for="p in products" :key="p.id">
    <ProductCard 
      :data="p" 
      :selected="selectedId === p.id"
      @click="selectedId = p.id"
    />
  </div>
</template>

In Nuxt 3.12 abbiamo implementato questo pattern in produzione — su una pagina categoria di un sito di moda, il TBT è sceso da 1840ms a 310ms. Il cambio critico è questo: l'array products non entra nel payload di hydration, quindi il bundle JavaScript iniziale è 41KB più piccolo. Ma poiché lo stato selectedId è lato client, c'è il rischio di mismatch di hydration — il server renderizza null, ma il client legge da localStorage un valore diverso. La soluzione è usare il wrapper <ClientOnly> oppure impostare lo stato nel hook onMounted.

Il Rischio di Hydration Mismatch e i Pattern di Soluzione

L'hydration mismatch accade quando l'HTML del server non corrisponde al primo render del client, e React/Vue rigenera il DOM. Aggiunge 200-300ms al TBT. Scenario di mismatch classico: renderizzi un timestamp con Date.now() sul server, ma il client produce un valore diverso con lo stesso codice.

Con RSC il rischio di mismatch è minore perché il server component non viene mai hydratato. Ma se usi dati provenienti dal server come prop di un client component, fai attenzione ai limiti di serializzazione. Gli oggetti Date diventano stringhe ISO, Map e Set non vengono serializzati. In Next.js 14 puoi definire una server function asincrona con la direttiva use server e chiamarla dal client:

// app/actions.ts
'use server'
export async function getCartTotal(userId: string) {
  const cart = await db.cart.findUnique({ where: { userId } })
  return cart.items.reduce((sum, i) => sum + i.price, 0)
}

// app/cart-summary.tsx (client component)
'use client'
import { getCartTotal } from './actions'

export default function CartSummary({ userId }: { userId: string }) {
  const [total, setTotal] = useState<number | null>(null)
  
  useEffect(() => {
    getCartTotal(userId).then(setTotal)
  }, [userId])
  
  return <span>{total ?? '...'}</span>
}

In questo pattern non c'è hydration — il client renderizza null al primo render, poi aggiorna lo stato quando arriva la risposta della server action. Contribuisce ~10ms al TBT (escludendo la latenza di rete).

RSC con Shopify Storefront: Quali Component Dove?

Alla fine del 2025, Shopify Hydrogen 2.0 ha reso RSC il default. Le domande classiche rimangono: la product card è server o client? L'icona del carrello è server o client? Il bottone add-to-cart è sicuramente client, ma la logica di lazy-load dell'immagine del prodotto può stare sul server?

In un progetto di Headless Commerce per un brand di cosmetici, Roibase ha preso queste decisioni:

ComponentePosizionamentoMotivazione
ProductCard (immagine + prezzo)ServerDati statici, hydration cost 40ms, TTFB +9ms
AddToCart buttonClientFeedback immediato, notifiche toast
QuickView modalClientStato overlay, navigazione da tastiera
SizeSelectorIbridoOpzioni dal server, stato della selezione sul client
RelatedProductsServerRaccomandazioni statiche, API call server-side

Risultato: LCP è sceso da 2.8s a 1.4s (dato 90° percentile Shopify Analytics). Però l'animazione di apertura del modal è scesa da 60fps a 45fps — abbiamo dovuto tenere il component QuickView sul client perché l'animazione CSS viene attivata in runtime.

La Matrice di Decisione: Quali Segnali Indicano Quale Lato?

La tabella seguente mostra i segnali che guidano la decisione server/client per ogni component:

Sposta sul server:

  • I prop del component provengono da database/API e non dipendono da interazioni utente
  • La logica di rendering è CPU-intensive (parse markdown, syntax highlighting)
  • È contenuto critico per la SEO (descrizione prodotto, corpo articolo)
  • La dimensione del bundle > 15KB e non è necessaria al first paint

Tieni sul client:

  • È richiesto feedback utente immediato (validazione form, toast)
  • Dipende dalle API del browser (localStorage, IntersectionObserver)
  • Le animazioni/transizioni sono attivate in runtime (modal, drawer)
  • C'è un re-render frequente (input di ricerca, slider)

Ibrido (server component + client island):

  • Fetch dei dati sul server, logica di interazione sul client (le opzioni del dropdown arrivano dal server, lo stato della selezione è sul client)
  • Shell statica sul server, contenuto dinamico sul client (skeleton della product card sul server, prezzo/disponibilità sul client)

Abbiamo applicato questa matrice in 12 progetti Next.js + RSC — il miglioramento medio del TBT è del 73%, la regressione media del TTFB è dell'8% (trade-off accettabile).

Edge Case: Personalizzazione e il Limite del Server Component

Un limite del server component è questo: non puoi renderizzare lo stato specifico dell'utente perché il rendering del server viene messo in cache. Esempio: un widget "Prodotti per te" deve essere diverso per ogni utente. Con RSC ci sono due soluzioni:

  1. Server action + client state: La shell del widget è sul server, il contenuto viene fetched sul client (come nell'esempio del totale del carrello sopra).
  2. Edge middleware per la personalizzazione: Usa Cloudflare Workers oppure Vercel Edge Functions per leggere il segmento utente dagli header della request e iniettare il contenuto prima che il server renderizzi l'HTML.

Il secondo approccio è più veloce (latenza edge < 50ms) ma l'edge runtime non supporta le API Node.js — non puoi usare il bundle del client database. Nel 2026, poiché Cloudflare D1 e Vercel Postgres sono nativi per edge, questo limite sta scomparendo.

Esempio di edge middleware (Next.js 15):

// middleware.ts
import { NextResponse } from 'next/server'

export function middleware(request: Request) {
  const segment = request.headers.get('x-user-segment') || 'default'
  const response = NextResponse.next()
  response.headers.set('x-personalization', segment)
  return response
}

Il server component legge questo header e renderizza i dati specifici del segmento. Poiché il segmento viene aggiunto alla chiave di cache, ogni segmento ha il suo entry di cache separato.

Scelta degli Strumenti nel 2026: Next, Nuxt, Remix — Dove Cosa?

RSC non è più agnostico dal framework — ogni framework ha la sua interpretazione:

  • Next.js 15: Il supporto RSC più maturo, App Router stabile, le server action sono una feature di prima classe. Trade-off: rischio di lock-in Vercel, self-host dell'edge runtime è difficile.
  • Nuxt 3.12: Con Vue 3.5, <script setup server> e Nitro server unificato. Trade-off: non è granulare come RSC, non c'è la divisione server/client a livello di component.
  • Remix 2.8: Il pattern loader/action è vicino a RSC ma la divisione tra client component non è netta. Trade-off: navigazione SPA veloce, initial load lento.
  • SvelteKit 2.5: Il pattern +page.server.ts è simile a RSC. Trade-off: Svelte 5 runes hanno ancora bassa adozione nell'ecosistema.

Nei progetti Roibase, a partire dal 2026, usiamo Next.js nel 60% dei casi, Nuxt nel 30%, Remix nel 10%. Il criterio di scelta è: stack esistente (React vs Vue), competenza del team, target di deploy (Vercel/Cloudflare/self-hosted).

L'architettura server component è ormai il default — la domanda non è più "dovrei usarla?", ma "come la ottimizo?". La matrice di decisione e la mappa dei trade-off sopra riportate ancorano ogni scelta di server/client a criteri numerici. Nel 2026, tracciare la linea giusta significa raggiungere TBT < 200ms e LCP < 1.5s — e il modo fondamentale per farlo è scegliere consapevolmente dove renderizzare ogni component.