Ricostruzione di un grande sito di e-commerce con Next.js (Case Study)
Pubblicato: 2022-03-10Nella nostra azienda, Unplatform, costruiamo siti di e-commerce da decenni ormai. In questi anni, abbiamo visto lo stack tecnologico evolversi da pagine rese dal server con alcuni JavaScript e CSS minori ad applicazioni JavaScript in piena regola.
La piattaforma che abbiamo utilizzato per i nostri siti di e-commerce era basata su ASP.NET e quando i visitatori hanno iniziato ad aspettarsi una maggiore interazione, abbiamo aggiunto React per il front-end. Sebbene mescolare i concetti di un framework Web server come ASP.NET con un framework Web lato client come React abbia reso le cose più complicate, siamo rimasti abbastanza soddisfatti della soluzione. Questo fino a quando non siamo passati alla produzione con il nostro cliente con il traffico più alto. Dal momento in cui siamo andati in onda, abbiamo riscontrato problemi di prestazioni . I Core Web Vitals sono importanti, ancor di più nell'e-commerce. In questo studio di Deloitte: Milliseconds Make Millions, i ricercatori hanno analizzato i dati dei siti mobili di 37 marchi diversi. Di conseguenza, hanno scoperto che un miglioramento delle prestazioni di 0,1 secondi può portare a un aumento del 10% della conversione.
Per mitigare i problemi di prestazioni, abbiamo dovuto aggiungere molti server aggiuntivi (non preventivati) e abbiamo dovuto memorizzare nella cache le pagine in modo aggressivo su un proxy inverso. Ciò ci ha persino richiesto di disabilitare parti delle funzionalità del sito. Abbiamo finito per avere una soluzione davvero complicata e costosa che in alcuni casi serviva solo staticamente alcune pagine.
Ovviamente, questo non sembrava giusto, finché non abbiamo scoperto Next.js . Next.js è un framework Web basato su React che ti consente di generare pagine staticamente, ma puoi anche utilizzare il rendering lato server, rendendolo ideale per l'e-commerce. Può essere ospitato su un CDN come Vercel o Netlify, il che si traduce in una latenza inferiore . Vercel e Netlify utilizzano anche funzioni serverless per il rendering lato server, che è il modo più efficiente per scalare.
Sfide
Lo sviluppo con Next.js è fantastico, ma ci sono sicuramente alcune sfide. L'esperienza dello sviluppatore con Next.js è qualcosa che devi solo provare. Il codice che scrivi viene visualizzato istantaneamente nel tuo browser e la produttività vola nel cielo. Questo è anche un rischio perché puoi facilmente concentrarti troppo sulla produttività e trascurare la manutenibilità del tuo codice. Nel tempo, questo e la natura non tipizzata di JavaScript possono portare al degrado della tua base di codice. Il numero di bug aumenta e la produttività inizia a diminuire.
Può anche essere impegnativo dal punto di vista del runtime . Le più piccole modifiche al codice possono portare a un calo delle prestazioni e ad altri Core Web Vital. Inoltre, l'uso incauto del rendering lato server può portare a costi di servizio imprevisti.
Diamo uno sguardo più da vicino alle nostre lezioni apprese nel superare queste sfide.
- Modularizza la tua base di codice
- Pelucchi e formatta il tuo codice
- Usa TypeScript
- Pianifica le prestazioni e misura le prestazioni
- Aggiungi i controlli delle prestazioni al tuo Quality Gate
- Aggiungi test automatici
- Gestisci in modo aggressivo le tue dipendenze
- Utilizzare un servizio di aggregazione dei registri
- La funzionalità di riscrittura di Next.js consente l'adozione incrementale
Lezione appresa: modularizzare la base di codice
Framework front-end come Next.js rendono così facile iniziare in questi giorni. Esegui semplicemente npx create-next-app e puoi iniziare a scrivere codice. Ma se non stai attento e inizi a eliminare il codice senza pensare al design, potresti finire con una grande palla di fango.
Quando esegui npx create-next-app
, avrai una struttura di cartelle come la seguente (questo è anche il modo in cui è strutturata la maggior parte degli esempi):
/public logo.gif /src /lib /hooks useForm.js /api content.js /components Header.js Layout.js /pages Index.js
Abbiamo iniziato usando la stessa struttura. Avevamo alcune sottocartelle nella cartella dei componenti per i componenti più grandi, ma la maggior parte dei componenti era nella cartella dei componenti radice. Non c'è niente di sbagliato in questo approccio e va bene per progetti più piccoli. Tuttavia, con la crescita del nostro progetto, è diventato più difficile ragionare sui componenti e su dove vengono utilizzati. Abbiamo anche trovato componenti che non venivano più utilizzati! Promuove anche una grande palla di fango, perché non ci sono indicazioni chiare su quale codice dovrebbe dipendere da quale altro codice.
Per risolvere questo problema, abbiamo deciso di refactoring della codebase e raggruppare il codice in base a moduli funzionali (un po' come i moduli NPM) invece di concetti tecnici:
/src /modules /catalog /components productblock.js /checkout /api cartservice.js /components cart.js
In questo piccolo esempio, c'è un modulo checkout e un modulo catalogo. Raggruppare il codice in questo modo porta a una migliore rilevabilità: semplicemente osservando la struttura delle cartelle si sa esattamente che tipo di funzionalità si trova nella base di codice e dove trovarla. Rende anche molto più facile ragionare sulle dipendenze . Nella situazione precedente, c'erano molte dipendenze tra i componenti. Abbiamo ricevuto richieste pull di modifiche al checkout che hanno avuto un impatto anche sui componenti del catalogo. Ciò ha aumentato il numero di conflitti di unione e ha reso più difficile apportare modifiche.
La soluzione che ha funzionato meglio per noi è stata quella di mantenere le dipendenze tra i moduli al minimo assoluto (se hai davvero bisogno di una dipendenza, assicurati che sia unidirezionale) e introdurre un livello di "progetto" che lega tutto insieme:
/src /modules /common /atoms /lib /catalog /components productblock.js /checkout /api cartservice.js /components cart.js /search /project /layout /components /templates productdetail.js cart.js /pages cart.js
Una panoramica visiva di questa soluzione:
Il livello di progetto contiene il codice per il layout del sito e-commerce e i modelli di pagina. In Next.js, un componente di pagina è una convenzione e risulta in una pagina fisica. Nella nostra esperienza, queste pagine hanno spesso bisogno di riutilizzare la stessa implementazione ed è per questo che abbiamo introdotto il concetto di "modelli di pagina". I modelli di pagina utilizzano i componenti dei diversi moduli, ad esempio, il modello di pagina dei dettagli del prodotto utilizzerà i componenti del catalogo per visualizzare le informazioni sul prodotto, ma anche un componente Aggiungi al carrello dal modulo di pagamento.
Abbiamo anche un modulo comune, perché c'è ancora del codice che deve essere riutilizzato dai moduli funzionali. Contiene atomi semplici che sono componenti React utilizzati per fornire un aspetto coerente. Contiene anche codice dell'infrastruttura, pensa a determinati hook di reazione generici o al codice client GraphQL.
Avvertenza : assicurati che il codice nel modulo comune sia stabile e pensaci sempre due volte prima di aggiungere codice qui, per evitare che il codice si aggrovigli.
Micro front-end
In soluzioni ancora più grandi o quando si lavora con team diversi, può avere senso suddividere ulteriormente l'applicazione nei cosiddetti micro-frontend. In breve, ciò significa suddividere ulteriormente l'applicazione in più applicazioni fisiche ospitate in modo indipendente su URL diversi. Ad esempio: checkout.mydomain.com
e catalog.mydomain.com. Questi vengono poi integrati da una diversa applicazione che funge da proxy.
La funzionalità di riscrittura di Next.js è ottima per questo e il suo utilizzo in questo modo è supportato dalle cosiddette Multi Zone.
Il vantaggio delle multi-zone è che ogni zona gestisce le proprie dipendenze. Inoltre, semplifica l'evoluzione incrementale della base di codice: se viene rilasciata una nuova versione di Next.js o React, puoi aggiornare le zone una per una invece di dover aggiornare l'intera base di codice in una volta. In un'organizzazione multi-team, questo può ridurre notevolmente le dipendenze tra i team.
Ulteriori letture
- "Struttura del progetto Next.js", Yannick Wittwer, Medium
- "Una guida del 2021 su come strutturare il tuo progetto Next.js in modo flessibile ed efficiente", Vadorequest, Dev.to.
- "Micro frontend", Michael Geers
Lezione appresa: pelucchi e formatta il tuo codice
Questo è qualcosa che abbiamo imparato in un progetto precedente: se lavori nella stessa base di codice con più persone e non usi un formattatore, il tuo codice diventerà presto molto incoerente. Anche se stai usando le convenzioni di codifica e stai facendo delle revisioni, presto inizierai a notare i diversi stili di codifica, dando un'impressione disordinata del codice.
Un linter controllerà il tuo codice per potenziali problemi e un formattatore si assicurerà che il codice sia formattato in modo coerente. Usiamo ESLint & Pretty e pensiamo che siano fantastici. Non devi pensare allo stile di codifica, riducendo il carico cognitivo durante lo sviluppo.
Fortunatamente, Next.js 11 ora supporta ESLint pronto all'uso (https://nextjs.org/blog/next-11), rendendolo semplicissimo da configurare eseguendo npx next lint. Questo ti fa risparmiare un sacco di tempo perché viene fornito con una configurazione predefinita per Next.js. Ad esempio, è già configurato con un'estensione ESLint per React. Ancora meglio, viene fornito con una nuova estensione specifica di Next.js che individuerà persino problemi con il codice che potrebbero potenzialmente influire sui Core Web Vitals della tua applicazione! In un paragrafo successivo parleremo dei controlli di qualità che possono aiutarti a prevenire il push del codice su un prodotto che danneggia accidentalmente i tuoi Core Web Vitals. Questa estensione ti fornisce un feedback molto più velocemente, rendendola un'ottima aggiunta.
Ulteriori letture
- "ESLint", Next.js Docs
- "ESLint", sito ufficiale
Lezione appresa: utilizzare TypeScript
Quando i componenti sono stati modificati e refactoring, abbiamo notato che alcuni degli oggetti di scena dei componenti non venivano più utilizzati. Inoltre, in alcuni casi, abbiamo riscontrato bug a causa di tipi di oggetti di scena mancanti o errati passati ai componenti.
TypeScript è un superset di JavaScript e aggiunge tipi, che consentono a un compilatore di controllare staticamente il codice, un po' come un linter sotto steroidi.
All'inizio del progetto, non abbiamo davvero visto il valore dell'aggiunta di TypeScript. Abbiamo pensato che fosse solo un'astrazione non necessaria. Tuttavia, uno dei nostri colleghi ha avuto buone esperienze con TypeScript e ci ha convinto a provarlo. Fortunatamente, Next.js ha un ottimo supporto TypeScript pronto all'uso e TypeScript ti consente di aggiungerlo alla tua soluzione in modo incrementale. Ciò significa che non devi riscrivere o convertire l'intera base di codice in una volta sola, ma puoi iniziare a utilizzarla immediatamente e convertire lentamente il resto della base di codice.
Dopo aver iniziato a migrare i componenti a TypeScript, abbiamo immediatamente riscontrato problemi con il passaggio di valori errati a componenti e funzioni. Inoltre, il ciclo di feedback degli sviluppatori si è accorciato e si riceve una notifica dei problemi prima di eseguire l'app nel browser. Un altro grande vantaggio che abbiamo riscontrato è che rende molto più semplice il refactoring del codice: è più facile vedere dove viene utilizzato il codice e si individuano immediatamente i componenti e il codice inutilizzati. In breve, i vantaggi di TypeScript:
- Riduce il numero di bug
- Semplifica il refactoring del codice
- Il codice diventa più facile da leggere
Ulteriori letture
- "TypeScript", Next.js Docs
- TypeScript, sito ufficiale
Lezione appresa: pianificare le prestazioni e misurare le prestazioni
Next.js supporta diversi tipi di pre-rendering: generazione statica e rendering lato server. Per ottenere prestazioni ottimali, si consiglia di utilizzare la generazione statica, che avviene durante la fase di compilazione, ma ciò non è sempre possibile. Pensa alle pagine dei dettagli del prodotto che contengono informazioni sulle scorte. Questo tipo di informazioni cambia spesso e l'esecuzione di una build ogni volta non si adatta bene. Fortunatamente, Next.js supporta anche una modalità chiamata Rigenerazione statica incrementale (ISR), che genera ancora staticamente la pagina, ma ne genera una nuova in background ogni x secondi. Abbiamo appreso che questo modello funziona alla grande per applicazioni più grandi. Le prestazioni sono comunque ottime, richiede meno tempo di CPU rispetto al rendering lato server e riduce i tempi di compilazione: le pagine vengono generate solo alla prima richiesta. Per ogni pagina che aggiungi, dovresti pensare al tipo di rendering necessario. Innanzitutto, verifica se puoi utilizzare la generazione statica; in caso contrario, scegli la rigenerazione statica incrementale e, se anche questo non è possibile, puoi comunque utilizzare il rendering lato server.
Next.js determina automaticamente il tipo di rendering in base all'assenza dei metodi getServerSideProps
e getInitialProps
nella pagina. È facile commettere un errore, che potrebbe causare il rendering della pagina sul server invece di essere generata staticamente. L'output di una build Next.js mostra esattamente quale pagina utilizza quale tipo di rendering, quindi assicurati di controllarlo. Aiuta anche a monitorare la produzione e tenere traccia delle prestazioni delle pagine e del tempo di CPU coinvolto. La maggior parte dei provider di hosting ti addebita in base al tempo della CPU e questo aiuta a prevenire spiacevoli sorprese. Descriverò come lo monitoriamo nella lezione appresa: utilizzare un paragrafo del servizio di aggregazione dei log.
Dimensione del pacco
Per avere una buona prestazione è fondamentale ridurre al minimo le dimensioni del pacco. Next.js ha molte funzionalità pronte all'uso che aiutano, ad esempio la divisione automatica del codice. Ciò assicurerà che per ogni pagina vengano caricati solo JavaScript e CSS richiesti. Genera anche diversi bundle per il client e per il server. Tuttavia, è importante tenere d'occhio questi. Ad esempio, se si importano moduli JavaScript nel modo sbagliato, il JavaScript del server può finire nel bundle del client, aumentando notevolmente le dimensioni del bundle del client e danneggiando le prestazioni. L'aggiunta di dipendenze NPM può anche influire notevolmente sulle dimensioni del pacchetto.
Fortunatamente, Next.js viene fornito con un analizzatore di bundle che fornisce informazioni su quale codice occupa quale parte dei bundle.
Ulteriori letture
- "Next.js + Analizzatore di pacchetti Webpack", Vercel, GitHub
- "Recupero dei dati", Next.js Docs
Lezione appresa: aggiungi i controlli delle prestazioni al tuo Quality Gate
Uno dei grandi vantaggi dell'utilizzo di Next.js è la capacità di generare pagine statiche e di essere in grado di distribuire l'applicazione all'edge (CDN), che dovrebbe tradursi in grandi prestazioni e Web Vitals. Abbiamo imparato che, anche con una tecnologia eccezionale come Next.js, ottenere e mantenere un ottimo punteggio per il faro è davvero difficile. È successo diverse volte che, dopo aver implementato alcune modifiche alla produzione, il punteggio del faro è diminuito in modo significativo. Per riprendere il controllo, abbiamo aggiunto i test automatici del faro al nostro cancello di qualità. Con questa azione Github puoi aggiungere automaticamente i test del faro alle tue richieste pull. Stiamo utilizzando Vercel e ogni volta che viene creata una richiesta pull, Vercel la distribuisce a un URL di anteprima e utilizziamo l'azione Github per eseguire test faro su questa distribuzione.
Se non vuoi impostare tu stesso l'azione GitHub, o se vuoi andare ancora oltre, potresti anche prendere in considerazione un servizio di monitoraggio delle prestazioni di terze parti come DebugBear. Vercel offre anche una funzione di analisi, che misura i principali Web Vitals della distribuzione di produzione. Vercel Analytics raccoglie effettivamente le misure dai dispositivi dei tuoi visitatori, quindi questi punteggi sono davvero ciò che stanno vivendo i tuoi visitatori. Al momento della scrittura, Vercel Analytics funziona solo su distribuzioni di produzione.
Lezione appresa: aggiungere test automatizzati
Quando la base di codice diventa più grande, diventa più difficile determinare se le modifiche al codice potrebbero aver interrotto la funzionalità esistente. Nella nostra esperienza, è fondamentale disporre di una buona serie di test end-to-end come rete di sicurezza. Anche se hai un piccolo progetto, può semplificarti la vita quando hai almeno alcuni test di fumo di base. Abbiamo usato Cypress per questo e lo adoriamo assolutamente. La combinazione dell'utilizzo di Netlify o Vercel per distribuire automaticamente la tua richiesta Pull in un ambiente temporaneo ed eseguire i test E2E non ha prezzo.
Usiamo cypress-io/GitHub-action
per eseguire automaticamente i test di cipresso rispetto alle nostre richieste pull. A seconda del tipo di software che stai costruendo, può essere utile avere anche test più granulari usando Enzima o JEST. Il compromesso è che questi sono più strettamente accoppiati al tuo codice e richiedono più manutenzione.
Lezione appresa: gestisci in modo aggressivo le tue dipendenze
La gestione delle dipendenze diventa un'attività dispendiosa in termini di tempo, ma davvero importante quando si mantiene una grande base di codice Next.js. NPM ha reso l'aggiunta di pacchetti così semplice e sembra esserci un pacchetto per tutto in questi giorni. Guardando indietro, molte volte quando abbiamo introdotto un nuovo bug o abbiamo avuto un calo delle prestazioni, ciò aveva a che fare con un pacchetto NPM nuovo o aggiornato.
Quindi prima di installare un pacchetto dovresti sempre chiederti quanto segue:
- Qual è la qualità del pacchetto?
- Cosa significherà l'aggiunta di questo pacchetto per le dimensioni del mio pacchetto?
- Questo pacchetto è davvero necessario o ci sono alternative?
- Il pacchetto è ancora mantenuto attivamente?
Per mantenere piccole le dimensioni del pacchetto e per ridurre al minimo lo sforzo necessario per mantenere queste dipendenze, è importante mantenere il numero di dipendenze il più piccolo possibile. Il tuo sé futuro ti ringrazierà per questo durante la manutenzione del software.
Suggerimento : l'estensione Import Cost VSCode mostra automaticamente le dimensioni dei pacchetti importati.
Resta al passo con le versioni di Next.js
Tenere il passo con Next.js & React è importante. Non solo ti darà accesso a nuove funzionalità, ma le nuove versioni includeranno anche correzioni di bug e correzioni per potenziali problemi di sicurezza. Fortunatamente, Next.js rende l'aggiornamento incredibilmente semplice fornendo Codemods (https://nextjs.org/docs/advanced-features/codemods. Si tratta di trasformazioni automatiche del codice che aggiornano automaticamente il codice.
Aggiorna dipendenze
Per lo stesso motivo, è importante mantenere attuali le versioni Next.js e React; è anche importante aggiornare altre dipendenze. Il dependabot di Github (https://github.com/dependabot) può davvero aiutare qui. Creerà automaticamente richieste pull con dipendenze aggiornate. Tuttavia, l'aggiornamento delle dipendenze può potenzialmente interrompere le cose, quindi avere test end-to-end automatizzati qui può davvero essere un vero toccasana.
Lezione appresa: utilizzare un servizio di aggregazione dei registri
Per assicurarci che l'app funzioni correttamente e per trovare preventivamente i problemi, abbiamo riscontrato che è assolutamente necessario configurare un servizio di aggregazione dei log. Vercel ti consente di accedere e visualizzare i log, ma questi vengono trasmessi in streaming in tempo reale e non vengono mantenuti. Inoltre, non supporta la configurazione di avvisi e notifiche.
Alcune eccezioni possono richiedere molto tempo per emergere. Ad esempio, abbiamo configurato Stale-While-Revalidate per una pagina particolare. Ad un certo punto, abbiamo notato che le pagine non venivano aggiornate e che i vecchi dati venivano serviti. Dopo aver controllato la registrazione di Vercel, abbiamo riscontrato che si stava verificando un'eccezione durante il rendering in background della pagina. Utilizzando un servizio di aggregazione dei log e configurando un avviso per le eccezioni, saremmo stati in grado di individuarlo molto prima.
I servizi di aggregazione dei log possono essere utili anche per monitorare i limiti dei piani tariffari di Vercel. Anche la pagina di utilizzo di Vercel fornisce informazioni dettagliate su questo, ma l'utilizzo di un servizio di aggregazione dei registri consente di aggiungere notifiche quando si raggiunge una determinata soglia. Prevenire è meglio che curare, soprattutto quando si tratta di fatturazione.
Vercel offre una serie di integrazioni pronte all'uso con i servizi di aggregazione dei registri, tra cui Datadog, Logtail, Logalert, Sentry e altro ancora.
Ulteriori letture
- "Integrazioni", Vercel
Lezione appresa: la funzionalità di riscrittura di Next.js consente l'adozione incrementale
A meno che non ci siano problemi seri con il sito Web attuale, non molti clienti saranno entusiasti di riscrivere l'intero sito Web. Ma cosa accadrebbe se potessi iniziare con la ricostruzione solo delle pagine che contano di più in termini di Web Vitals? Questo è esattamente quello che abbiamo fatto per un altro cliente. Invece di ricostruire l'intero sito, ricostruiamo solo le pagine che contano di più per SEO e conversione. In questo caso le pagine dei dettagli del prodotto e della categoria. Ricostruendo quelli con Next.js, le prestazioni sono notevolmente aumentate.
La funzionalità di riscrittura di Next.js è ottima per questo. Abbiamo creato un nuovo front-end Next.js che contiene le pagine del catalogo e lo abbiamo distribuito sulla CDN. Tutte le altre pagine esistenti vengono riscritte da Next.js sul sito Web esistente. In questo modo puoi iniziare ad avere i vantaggi di un sito Next.js in modo semplice o a basso rischio.
Ulteriori letture
- "Riscrive", Next.js Docs
Qual è il prossimo?
Quando abbiamo rilasciato la prima versione del progetto e abbiamo iniziato a fare seri test delle prestazioni, siamo rimasti entusiasti dei risultati. Non solo i tempi di risposta della pagina e i Web Vital erano molto migliori di prima, ma anche i costi operativi erano una frazione di quello di prima. Next.js e JAMStack generalmente ti consentono di scalare in orizzontale nel modo più efficiente in termini di costi.
Il passaggio da un'architettura più orientata al back-end a qualcosa come Next.js è un grande passo. La curva di apprendimento può essere piuttosto ripida e inizialmente alcuni membri del team si sono sentiti davvero fuori dalla loro zona di comfort. I piccoli aggiustamenti che abbiamo apportato, le lezioni apprese da questo articolo, hanno davvero aiutato in questo. Inoltre, l'esperienza di sviluppo con Next.js offre un incredibile aumento della produttività. Il ciclo di feedback degli sviluppatori è incredibilmente breve!
Ulteriori letture
- "Andare in produzione", Next.js Docs