← Torna al blog
EngineeringBuild in PublicSEO

Le nostre pagine Next.js erano troppo pesanti per Bing. Il payload RSC era il 58 % dell HTML

Tre pagine Next.js ferme su scoperta ma non scansionata in Bing. L App Router spedisce ogni byte due volte. Ne abbiamo alleggerita una di 48 KB e Bing l ha indicizzata.

8 ottobre 2026

Le nostre pagine Next.js erano troppo pesanti per Bing. Il payload RSC era il 58 % dell HTML

Katto è uno strumento di clipping video con IA. Gli dai un video lungo, lui trova i momenti migliori e li taglia in clip verticali sottotitolate. Lo costruisco da solo, in pubblico, con Next.js e l'App Router. Questa è una misurazione che ha cambiato il mio modo di scrivere pagine, un numero che ho dato per buono davanti a tutti e che era sbagliato, e una mia ipotesi che valeva 108 byte.

Il sintomo

Bing Webmaster Tools segnalava diverse nostre pagine come scoperte ma mai scansionate. Non bloccate da robots.txt, non in noindex, non lente, non reindirizzate. Scoperte e poi abbandonate. Google le indicizzava. La nostra sitemap le elencava. Rispondevano 200 a chiunque le chiedesse.

Le pagine rifiutate avevano una cosa in comune, e non era il loro contenuto.

Il limite, e quanto poco lo conosciamo

Microsoft non pubblica alcuna dimensione oltre la quale il suo crawler rinuncia. Quello che abbiamo sono le nostre pagine, ciascuna con lo stato letto in Bing Webmaster Tools e il peso servito misurato lo stesso giorno, senza compressione, con lo user agent di bingbot:

Una sola pagina dall'altra parte di una linea non è una legge. Ma in quell'elenco c'è un'osservazione che vale più di tutto il resto, perché è due volte lo stesso URL. /compare/ai-video-clippers serviva 172 295 byte ed è rimasta per settimane ferma su scoperta ma non scansionata. L'abbiamo ridotta a circa 124 KB. Oggi è indicizzata, a 127 951 byte. Stesso URL, stesso template, stesso tipo di contenuto: troppo grande, poi abbastanza piccola.

Una settimana fa ti avrei dato un altro numero, e sarebbe stato sbagliato. Avevo tre letture di stato e un buco ben visibile nella distribuzione delle nostre dimensioni, e ci ho letto dentro un limite vicino ai 125 KB. Due di quelle pagine sono state da allora indicizzate a poco meno di 128 KB. Quello che il nostro sito può davvero dire è che la linea sta tra 128 e 140 KB, e che il suo lato opposto è ancora largo esattamente una pagina.

Se sei venuto per un numero, la risposta onesta è di misurare il tuo. L'affermazione che posso difendere è comunque più stretta e più utile: una pagina ferma su scoperta merita di essere pesata prima di essere riscritta, e una pagina che comincia a essere scansionata dopo essere dimagrita è l'unica prova che conta davvero.

Pesare il file sorgente non dice nulla

Ecco la parte specifica dei React Server Components. L'App Router serializza l'albero renderizzato dentro lo stesso documento HTML, come una sequenza di tag self.__next_f.push(...), perché una navigazione lato client possa riprendere senza un altro giro di richiesta. Quel payload non è una richiesta separata che un crawler può saltare. È nel documento.

Misurato su una delle nostre pagine: 125 981 byte serviti, di cui 73 571 sono quel payload. Il cinquantotto per cento del documento è la copia serializzata di ciò che il restante quarantadue per cento dice già.

Quindi una classe di utilità scritta una sola volta dentro un ciclo di 24 righe non viene spedita 24 volte. Ne viene spedita 48.

La parte che ho impiegato più tempo a vedere

Non raddoppia la pagina. Raddoppia ogni rappresentazione che la pagina già porta.

La nostra roadmap renderizzava di proposito ogni voce due volte: una come testo visibile, una dentro un blocco ItemList di JSON-LD, perché i lettori macchina ricevessero nome, data e stato come dati invece che come prosa da interpretare. Ragionevole. Poi ho contato le occorrenze della descrizione di una singola voce nel documento servito e ne ho trovate quattro.

Due rappresentazioni, ciascuna raddoppiata. Quattro copie della stessa frase, e non mi era mai venuto in mente di contarle.

La correzione non era comprimere. Il JSON-LD portava una description che copiava parola per parola un testo che il lettore vede trenta righe più in basso, nella stessa lingua. Non aggiungeva nulla e costava due delle quattro copie. Rimossa, il blocco conserva nome, data e stato, cioè proprio quello che la prosa non dice esplicitamente a una macchina. Quella pagina è passata da 121 196 a 115 350 byte senza che ne uscisse alcuna informazione. Entrambi i numeri vengono dalla stessa build, prima e dopo quell'unica modifica, che è l'unico modo perché un prima e un dopo significhino qualcosa.

Se porti via una sola abitudine da qui: prima di provare ad alleggerire qualsiasi cosa, conta quante volte una frase del tuo contenuto compare nel documento servito. Risponde a una domanda a cui pesare le sezioni non risponde.

Le due leve più grosse

Gli attributi di classe ripetuti, portati in regole del foglio di stile. Le utility di Tailwind sono meravigliosamente economiche da scrivere, e viaggiano su ogni elemento, due volte. Spostare le più ripetute in regole @apply ha portato il nostro indice dei prezzi da 172 295 byte a circa 142 000. Erano all'incirca il 51 per cento del markup.

I blocchi non interattivi, fuori dall'albero serializzato. Per essere precisi, perché conta: sono Server Components, quindi non vengono mai idratati sul client. Il costo non è l'idratazione, è la serializzazione. Ogni elemento, ogni prop e ogni chiave viene scritto nel flusso perché una navigazione lato client possa ricostruire l'albero senza un giro di richiesta. Una tabella statica senza alcun comportamento client paga comunque quel prezzo, e quando il vincolo è la dimensione del documento non c'è motivo che resti fatta di elementi React. Ora costruiamo quei blocchi come una stringa HTML a partire dagli stessi dati, posata con dangerouslySetInnerHTML, facendo l'escape di ogni valore interpolato al momento della costruzione perché l'escape non dipenda da dove arriva il dato. Altri 18 KB sull'indice dei prezzi. Un elenco di 54 righe sulla roadmap è passato da 157 308 a 119 031 per la stessa strada.

Quello che non ha funzionato

Prima di tutto questo, ero sicuro che il problema fossero i confini dei componenti. Cinquantaquattro componenti next/link in un elenco, ciascuno che aggiunge al payload il proprio nome serializzato, le proprie prop e le proprie chiavi. Ho sostituito tutti e 54 con semplici ancore.

Ha risparmiato 108 byte su 157 200.

Link costa poco. Il contenuto che avvolge no. Avrei passato una giornata a riscrivere componenti su quella teoria se non avessi misurato prima e dopo, e quel risultato negativo è stato il numero più utile della giornata.

C'è un pavimento, e non è basso

La nostra pagina reale più leggera, /contact, serve 46 111 byte pur portando solo un titolo, un modulo breve e qualche domanda. Il resto è navigazione, piè di pagina, analytics e font. Sotto quella cifra non c'è più nulla da guadagnare pagina per pagina: sarebbe un lavoro sullo scafo, non sulla pagina. Conoscere il pavimento ti dice quando fermarti.

Una traduzione costa byte, non parole

Abbiamo messo quella roadmap in dieci lingue. Stesso markup, stessa struttura, stessa informazione. La pagina inglese serve 116 239 byte, quella francese 119 838, quella giapponese 122 155 e quella hindi 135 379.

Il devanagari occupa tre byte per carattere in UTF-8 dove il testo latino ne occupa uno, e il payload lo paga una seconda volta. Nessuna tecnica a livello di pagina tocca questo: stesso markup, stessa informazione, diciannove kilobyte di distanza.

La pagina hindi è quella interessante, perché con 135 379 byte cade proprio dentro la fascia che non sappiamo risolvere. Sapere se Bing la elabora è l'esperimento più economico a nostra disposizione, e costa un'ispezione di URL.

Avevo scritto qui un paragrafo per sostenere che quello sforamento non contasse molto, perché cade alla fine del documento e la fine di quella pagina è un elenco i cui titoli restano comunque in inglese. L'ho tagliato, perché presuppone che il crawler legga finché esaurisce il suo budget e poi si fermi. Tutto quello che ho davvero osservato dice soltanto che le pagine sopra una certa dimensione non vengono elaborate. Se il documento viene rifiutato per intero, il suo ordine non compra nulla, e non ho prove né in un senso né nell'altro. È una teoria comoda sulla mia stessa pagina, ed è il tipo che conviene cancellare per primo.

È un budget, non una correzione

L'indice dei prezzi che avevo portato da 172 295 a 124 202 byte oggi è a 127 951, perché abbiamo continuato ad aggiungerci roba: più strumenti, più colonne, dieci traduzioni. Resta indicizzato, che è tutto il punto, ed è anche tornato a pochi kilobyte dalla linea. Non è andato storto niente. Abbiamo semplicemente rispeso il budget senza sorvegliarlo.

Questa è la vera conclusione. In un framework che serializza il proprio output, il peso di una pagina non è una pulizia che si fa una volta. È un numero che sale ogni volta che qualcuno fa un buon lavoro, e l'unica difesa è misurare il documento servito invece del sorgente, una modifica alla volta.

Come misurare le tue

Articoli correlati

Pronto a trasformare i tuoi video in clip virali?

Katto taglia, sottotitola e riformatta automaticamente i tuoi video lunghi in contenuti brevi.

Prova Katto gratis →