Rendering sul Web

Dove dovremmo implementare la logica e il rendering nelle nostre applicazioni? Dovremmo utilizzare il rendering lato server? E la reidratazione? Vediamo alcune risposte!

In qualità di sviluppatori, ci troviamo spesso di fronte a decisioni che incidono sull'intera architettura delle nostre applicazioni. Una delle decisioni principali che gli sviluppatori web devono prendere è dove implementare la logica e il rendering nella loro applicazione. Questo può essere difficile, poiché esistono diversi modi per creare un sito web.

La nostra comprensione di questo ambito si basa sul lavoro svolto negli ultimi anni in Chrome in relazione a siti di grandi dimensioni. In generale, invitiamo gli sviluppatori a prendere in considerazione il rendering lato server o il rendering statico rispetto a un approccio completo di reidratazione.

Per comprendere meglio le architetture da cui prendiamo questa decisione, dobbiamo avere una solida comprensione di ciascun approccio e una terminologia coerente da utilizzare quando ne parliamo. Le differenze tra questi approcci aiutano a illustrare i compromessi del rendering sul web attraverso la lente del rendimento.

Terminologia

Rendering

  • Rendering lato server (SSR): eseguire il rendering di un'app lato client o universale in HTML sul server.
  • Rendering lato client (CSR): esegue il rendering di un'app in un browser tramite JavaScript per modificare il DOM.
  • Reidratazione: "avvio" delle visualizzazioni JavaScript sul client in modo da riutilizzare i dati e l'albero DOM dell'HTML sottoposto a rendering dal server.
  • Prerendering: è in esecuzione un'applicazione lato client in fase di creazione per acquisire il suo stato iniziale sotto forma di HTML statico.

Prestazioni

Rendering lato server

Il rendering lato server genera l'intero codice HTML di una pagina sul server in risposta alla navigazione. Ciò evita ulteriori round trip per il recupero dei dati e la creazione di modelli sul client, poiché vengono gestiti prima che il browser riceva una risposta.

Il rendering lato server generalmente produce un valore FCP veloce. L'esecuzione della logica e del rendering della pagina sul server consente di evitare l'invio di molto codice JavaScript al client. Ciò contribuisce a ridurre il TBT di una pagina, il che può anche portare a un INP inferiore, poiché il thread principale non viene bloccato tanto spesso durante il caricamento pagina. Quando il thread principale viene bloccato meno spesso, le interazioni degli utenti hanno più opportunità di essere eseguite prima. Questo ha senso, poiché con il rendering lato server, invii davvero solo testo e link al browser dell'utente. Questo approccio può essere efficace per un ampio spettro di condizioni del dispositivo e della rete e apre interessanti ottimizzazioni del browser come l'analisi dei documenti in streaming.

Diagramma che mostra il rendering lato server e l'esecuzione di JS che interessano FCP e TTI.

Con il rendering lato server, è meno probabile che gli utenti rimangano in attesa dell'esecuzione di JavaScript associato alla CPU prima di poter utilizzare il tuo sito. Anche nei casi in cui non è possibile evitare il codice JS di terze parti, l'utilizzo del rendering lato server per ridurre i costi JavaScript proprietari può comportare un budget maggiore per il resto. Tuttavia, esiste un potenziale compromesso con questo approccio: la generazione di pagine sul server richiede tempo, il che potrebbe comportare un TTFB più elevato.

La quantità di rendering lato server sufficiente per la tua applicazione dipende in gran parte dal tipo di esperienza che stai creando. Si è da tempo dibattuto sull'applicazione corretta del rendering lato server rispetto al rendering lato client, ma è importante ricordare che è possibile scegliere di utilizzare il rendering lato server per alcune pagine e non per altre. Alcuni siti hanno adottato con successo tecniche di rendering ibrido. Il server Netflix visualizza le sue pagine di destinazione relativamente statiche, precaricando al contempo il codice JS per quelle che generano molte interazioni, dando loro maggiori probabilità di caricarsi rapidamente.

Molti framework, librerie e architetture moderne permettono di eseguire il rendering della stessa applicazione sia sul client che sul server. Queste tecniche possono essere utilizzate per il rendering lato server. Tuttavia, è importante notare che le architetture in cui il rendering avviene sia sul server sia sul client rappresentano la propria classe di soluzioni con caratteristiche prestazionali e compromessi molto diversi. Gli utenti di React possono utilizzare API DOM server o soluzioni basate su di essi, come Next.js, per il rendering lato server. Gli utenti di Vue possono consultare la guida al rendering lato server di Vue o Nuxt. Angular ha il valore Universale. Le soluzioni più popolari utilizzano qualche forma di idratazione, quindi presta attenzione all'approccio in uso prima di scegliere uno strumento.

Rendering statico

Il rendering statico viene eseguito in fase di creazione. Questo approccio offre un FCP veloce, oltre a un TBT e un INP inferiori, supponendo che la quantità di JavaScript lato client sia limitata. A differenza del rendering lato server, riesce anche a raggiungere un TTFB costantemente veloce, in quanto il codice HTML di una pagina non deve essere generato dinamicamente sul server. In genere, il rendering statico implica la produzione anticipata di un file HTML separato per ogni URL. Con le risposte HTML generate in anticipo, è possibile eseguire il rendering statico su più CDN per sfruttare la memorizzazione in una cache perimetrale.

Diagramma che mostra il rendering statico e l'esecuzione facoltativa di JS che interessano FCP e TTI.

Le soluzioni per il rendering statico sono disponibili in tutte le forme e dimensioni. Strumenti come Gatsby sono progettati per far sentire gli sviluppatori che la loro applicazione venga visualizzata in modo dinamico, anziché essere generata come passaggio di build. Gli strumenti di generazione di siti statici come 11ty, Jekyll e Metalsmith adottano la loro natura statica, fornendo un approccio più basato su modelli.

Uno degli svantaggi del rendering statico è che devono essere generati singoli file HTML per ogni URL possibile. Ciò può essere difficile o addirittura impossibile se non puoi prevedere quali saranno questi URL in anticipo o nel caso di siti con un numero elevato di pagine uniche.

Gli utenti di React potrebbero avere familiarità con Gatsby, l'esportazione statica di Next.js o Navi: tutti questi passaggi semplificano la creazione di pagine utilizzando i componenti. Tuttavia, è importante comprendere la differenza tra rendering statico e prerendering: le pagine con rendering statico sono interattive senza la necessità di eseguire molto codice JavaScript lato client, mentre il prerendering migliora l'FCP di un'applicazione a pagina singola, che deve essere avviata sul client affinché le pagine siano veramente interattive.

Se non hai la certezza che una determinata soluzione sia di rendering statico o di prerendering, prova a disattivare JavaScript e caricare la pagina che vuoi testare. Per le pagine visualizzate in modo statico, la maggior parte delle funzionalità continuerà a esistere anche senza JavaScript attivato. Per le pagine sottoposte a prerendering, potrebbero essere comunque presenti alcune funzionalità di base come i link, ma la maggior parte della pagina sarà inerte.

Un altro test utile consiste nell'utilizzare la limitazione della rete in Chrome DevTools e osservare la quantità di codice JavaScript scaricato prima che una pagina diventi interattiva. In genere, il prerendering richiede più JavaScript per diventare interattivo e JavaScript tende a essere più complesso dell'approccio di miglioramento progressivo utilizzato dal rendering statico.

Rendering lato server e rendering statico

Il rendering lato server non è una soluzione miracolosa: la sua natura dinamica può comportare significativi costi generali di calcolo. Molte soluzioni di rendering lato server non eseguono lo svuotamento anticipato, possono ritardare il TTFB o raddoppiare i dati inviati (ad esempio, lo stato incorporato utilizzato da JavaScript sul client). In React, l'renderToString() può essere lento poiché è sincrono e a thread singolo. Le API DOM del server React più recenti supportano i flussi di dati, che possono ricevere prima la parte iniziale di una risposta HTML al browser mentre il resto viene ancora generata sul server.

Ottenere un rendering lato server "corretto" può comportare la ricerca o la creazione di una soluzione per la memorizzazione dei componenti, la gestione del consumo della memoria, l'applicazione di tecniche di memorizzazione e altri problemi. In genere l'elaborazione/ricostruzione della stessa applicazione viene eseguita più volte, una volta sul client e una volta sul server. Solo perché il rendering lato server può far apparire prima qualcosa non significa improvvisamente che hai meno lavoro da fare: se devi molto lavoro sul client dopo che una risposta HTML generata dal server arriva sul client, questo può comunque portare a un aumento del TBT e dell'INP per il tuo sito web.

Il rendering lato server produce HTML on demand per ogni URL, ma può essere più lento rispetto alla semplice pubblicazione di contenuti con rendering statico. Se puoi dedicarti al lavoro aggiuntivo, il rendering lato server e la memorizzazione nella cache HTML possono ridurre notevolmente i tempi di rendering del server. Il vantaggio del rendering lato server è la capacità di estrarre più dati "live" e rispondere a un insieme di richieste più completo di quanto non sia possibile con il rendering statico. Le pagine che richiedono una personalizzazione sono un esempio concreto del tipo di richiesta che non funzionerebbe bene con il rendering statico.

Il rendering lato server può anche presentare decisioni interessanti durante la creazione di una PWA: è meglio utilizzare la memorizzazione nella cache del service worker a pagina intera o semplicemente il rendering del server di singoli contenuti?

Rendering lato client

Il rendering lato client indica il rendering delle pagine direttamente nel browser con JavaScript. Tutta la logica, il recupero dei dati, i modelli e il routing vengono gestiti sul client anziché sul server. Il risultato è che una quantità maggiore di dati viene trasmessa al dispositivo dell'utente dal server, comportando una serie di compromessi.

Il rendering lato client può essere difficile da ottenere e mantenere veloce per i dispositivi mobili. Se viene eseguito un lavoro minimo, il rendering lato client è in grado di avvicinarsi alle prestazioni del rendering puro lato server, mantenendo un budget JavaScript ridotto e garantendo un valore aggiunto nel minor numero di round trip possibile. Gli script e i dati critici possono essere caricati prima utilizzando <link rel=preload>, che consente di velocizzare l'analisi del parser. Anche schemi come il PRPL devono essere valutati per far sì che la navigazione iniziale e successiva siano immediate.

Diagramma che mostra il rendering lato client che influisce su FCP e TTI.

Il principale svantaggio del rendering lato client è che la quantità di JavaScript richiesto tende ad aumentare con l'aumento di un'applicazione, il che può avere effetti negativi sull'INP di una pagina. Ciò diventa particolarmente difficile con l'aggiunta di nuove librerie JavaScript, polyfill e codice di terze parti, che competono per la potenza di elaborazione e spesso devono essere elaborati prima di poter visualizzare i contenuti di una pagina.

Le esperienze che utilizzano il rendering lato client che si basano su bundle JavaScript di grandi dimensioni dovrebbero prendere in considerazione una suddivisione aggressiva del codice per ridurre TBT e INP durante il caricamento della pagina e assicurarsi di caricare JavaScript mediante caricamento lento: "pubblica solo ciò che ti serve, quando ne hai bisogno". Per esperienze con interattività minima o nulla, il rendering lato server può rappresentare una soluzione più scalabile a questi problemi.

Per chi crea applicazioni a pagina singola, identificare le parti fondamentali dell'interfaccia utente condivise dalla maggior parte delle pagine significa poter applicare la tecnica di memorizzazione nella cache della shell dell'applicazione. Combinato con i service worker, questo può migliorare notevolmente le prestazioni percepite in caso di visite ripetute, poiché il codice HTML della shell dell'applicazione e le sue dipendenze possono essere caricati da CacheStorage molto rapidamente.

Combinazione di rendering lato server e rendering lato client tramite reidratazione

Questo approccio tenta di ottimizzare i compromessi tra il rendering lato client e quello lato server. Le richieste di navigazione, ad esempio i caricamenti di pagine intere o i ricaricamenti, vengono gestite da un server che esegue il rendering dell'applicazione in HTML, quindi il codice JavaScript e i dati utilizzati per il rendering vengono incorporati nel documento risultante. Se eseguita con attenzione, si ottiene un valore FCP veloce come il rendering lato server, quindi "viene ripreso" eseguendo il rendering di nuovo sul client utilizzando una tecnica chiamata (re)idratazione. Si tratta di una soluzione efficace, ma può presentare notevoli svantaggi in termini di rendimento.

Il principale svantaggio del rendering lato server con la reidratazione è che può avere un impatto negativo significativo su TBT e INP, anche se migliora il valore FCP. Le pagine visualizzate lato server possono sembrare ingannevoli e interattive, ma non possono rispondere all'input finché non vengono eseguiti gli script lato client per i componenti e non sono stati collegati i gestori di eventi. Sui dispositivi mobili questa operazione può richiedere alcuni secondi o persino minuti.

Magari è accaduto in prima persona. Per un periodo di tempo successivo al caricamento di una pagina, fare clic o toccare non ha alcun effetto. Ciò diventa rapidamente frustrante, poiché l'utente viene lasciato a chiedersi perché non succede nulla quando tenta di interagire con la pagina.

Un problema di reidratazione: un'app al prezzo di due

I problemi di reidratazione possono spesso essere peggiori rispetto all'interattività ritardata a causa di JavaScript. Affinché il codice JavaScript lato client sia in grado di "recuperare" accuratamente il punto in cui il server ha interrotto senza dover richiedere nuovamente tutti i dati utilizzati dal server per il rendering del codice HTML, le attuali soluzioni di rendering lato server generalmente serializzano la risposta dalle dipendenze dei dati di un'interfaccia utente nel documento come tag di script. Il documento HTML risultante contiene un elevato livello di duplicazione:

Documento HTML contenente UI serializzata, dati incorporati e uno script bundle.js

Come puoi vedere, il server restituisce una descrizione dell'interfaccia utente dell'applicazione in risposta a una richiesta di navigazione, ma restituisce anche i dati di origine utilizzati per comporre l'interfaccia utente e una copia completa dell'implementazione dell'interfaccia che viene avviata sul client. Questa UI diventa interattiva solo al termine del caricamento e dell'esecuzione di bundle.js.

Le metriche sul rendimento raccolte da siti web reali utilizzando il rendering e la reidratazione lato server indicano che l'uso del prodotto è sconsigliato. Essenzialmente, il motivo dipende dall'esperienza utente: è estremamente facile finire per lasciare gli utenti in una "vallata misteriosa", in cui l'interattività sembra assente anche se la pagina sembra essere pronta.

Diagramma che mostra il rendering del cliente che influisce negativamente sul TTI.

Tuttavia, c'è speranza per il rendering lato server con la reidratazione. Nel breve termine, solo l'utilizzo del rendering lato server per contenuti altamente memorizzabili nella cache può ridurre il TTFB, producendo risultati simili al prerendering. Reidratazione incrementale, progressiva o parziale può essere la chiave per rendere questa tecnica più utilizzabile in futuro.

Streaming di rendering lato server e reidratazione progressiva

Il rendering lato server ha avuto una serie di sviluppi negli ultimi anni.

Il rendering lato server dei flussi ti consente di inviare il codice HTML in blocchi che il browser può visualizzare progressivamente non appena lo riceve. Ciò può comportare un valore FCP veloce, in quanto il markup arriva più rapidamente agli utenti. In React, gli stream asincroni in [renderToPipeableStream()], rispetto ai canali renderToString() sincroni, indicano che la contropressione viene gestita bene.

Vale anche la pena di considerare la reidratazione progressiva, un prodotto che React è arrivato. Con questo approccio, le singole parti di un'applicazione sottoposta a rendering dal server vengono "avviate" nel tempo, invece che nell'attuale approccio comune di inizializzare l'intera applicazione contemporaneamente. Ciò può contribuire a ridurre la quantità di codice JavaScript richiesta per rendere interattive le pagine, poiché l'upgrade lato client di parti a bassa priorità della pagina può essere differito per evitare il blocco del thread principale, consentendo le interazioni degli utenti prima dell'avvio da parte dell'utente.

La reidratazione progressiva può anche aiutare a evitare uno dei più comuni errori di reidratazione nel rendering lato server, in cui un albero DOM sottoposto a rendering dal server viene distrutto e poi ricostruito immediatamente, il più delle volte perché il rendering sincrono iniziale lato client richiedeva dati non ancora pronti, forse in attesa della risoluzione di un Promise.

Reidratazione parziale

La reidratazione parziale si è dimostrata difficile da implementare. Questo approccio è un'estensione dell'idea di reidratazione progressiva, in cui i singoli pezzi (componenti/viste/alberi) da reidratare progressivamente vengono analizzati e vengono identificati quelli con scarsa interattività o nessuna reattività. Per ognuna di queste parti per lo più statiche, il codice JavaScript corrispondente viene poi trasformato in riferimenti inerti e funzionalità decorativa, riducendo l'impatto lato client quasi a zero.

L'approccio all'idratazione parziale porta con sé problemi e compromissioni. Presenta alcune sfide interessanti per la memorizzazione nella cache e la navigazione lato client significa che non possiamo presumere che l'HTML sottoposto a rendering dal server per parti inerte dell'applicazione sia disponibile senza un caricamento completo della pagina.

Rendering trisomorfico

Se i service worker sono un'opzione, potrebbe interessarti anche il rendering "trisomorfico". Si tratta di una tecnica in cui puoi utilizzare il rendering lato server di streaming per le navigazioni iniziali/non JS e quindi fare in modo che il tuo service worker si occupi del rendering del codice HTML per le navigazioni dopo l'installazione. In questo modo puoi mantenere aggiornati i componenti e i modelli memorizzati nella cache e consentire le navigazioni in stile SPA per eseguire il rendering di nuove viste nella stessa sessione. Questo approccio funziona al meglio quando puoi condividere lo stesso modello e lo stesso codice di routing tra server, pagina client e service worker.

Diagramma del rendering trisomorfico, che mostra un browser e un service worker che comunicano con il server.

Considerazioni sulla SEO

I team spesso tengono conto dell'impatto della SEO al momento di scegliere una strategia per il rendering sul web. Il rendering lato server viene spesso scelto per offrire un'esperienza di "aspetto completo" che i crawler possano interpretare con facilità. I crawler comprendono JavaScript, ma spesso esistono limitazioni che vale la pena conoscere relative al modo in cui vengono visualizzate. Il rendering lato client può funzionare, ma spesso non senza test aggiuntivi e un lavoro manuale. Più di recente, anche il rendering dinamico è diventato un'opzione che vale la pena prendere in considerazione se la tua architettura dipende molto da JavaScript lato client.

In caso di dubbi, lo strumento Test di ottimizzazione mobile è inestimabile per verificare che l'approccio scelto faccia ciò che ti aspetti. Mostra un'anteprima visiva di come ogni pagina appare al crawler di Google, dei contenuti HTML seriali trovati (dopo l'esecuzione di JavaScript) e di eventuali errori riscontrati durante il rendering.

Screenshot dell&#39;interfaccia utente del test di ottimizzazione mobile.

In sintesi

Quando decidi un approccio al rendering, misura e comprendi quali sono i colli di bottiglia. Considera se il rendering statico o lato server può garantirti la maggior parte del percorso. È perfettamente consentito inviare codice HTML con un numero minimo di JavaScript per offrire un'esperienza interattiva. Ecco una pratica infografica che mostra lo spettro server-client:

Infografica che mostra la gamma di opzioni descritte in questo articolo.

Crediti

Grazie a tutti per le recensioni e l'ispirazione:

Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson e Sebastian Markbåge