Websocket in Next.js: il tempo reale su hosting serverless

Aggiungere aggiornamenti in tempo reale a un’app Next.js di solito parte bene. Apri un websocket da un client component, lo punti verso un piccolo server, e con next dev funziona tutto: l’ordine passa a spedito, il nuovo messaggio compare, il numero di chi è online cambia. Poi fai il deploy, e la connessione cade, si riconnette, cade di nuovo, oppure non si apre proprio.
Il problema raramente è il codice. La maggior parte delle app Next.js gira su hosting serverless, e una funzione serverless non può tenere aperta una connessione. Questo articolo spiega perché, quanto costano le soluzioni di ripiego più comuni e come portare il tempo reale in un’app Next.js senza gestire un server websocket.
Perché una funzione serverless non può tenere aperto un websocket
Su un hosting serverless, un route handler, una server action e un server component funzionano tutti allo stesso modo. Arriva una richiesta, una funzione si avvia o si risveglia, risponde, e poco dopo viene congelata o fermata. La richiesta successiva può finire su un’istanza completamente diversa.
Un websocket ha bisogno dell’opposto: un processo che tenga aperta una connessione con ogni browser finché la pagina resta aperta, spesso per ore, e che possa inviarle dati in qualsiasi momento. Una funzione serverless non vive così a lungo, non ha modo di raggiungere una connessione aperta da un’altra istanza e viene fatturata per ogni secondo in cui resta attiva. Ecco perché tanti primi tentativi funzionano in sviluppo, dove un unico processo sempre in esecuzione serve tutto, e falliscono in produzione.
Le soluzioni di ripiego più comuni
Polling. La pagina chiede a un route handler ogni pochi secondi se è cambiato qualcosa. Funziona ovunque, ma ogni domanda è una chiamata a una funzione, nella maggior parte dei casi la risposta è che non c’è nulla di nuovo, e la pagina resta sempre indietro di qualche secondo. Con 1.000 persone su una pagina che fa polling ogni 5 secondi, sono 200 chiamate a funzione al secondo, 24 ore su 24.
Streaming da un route handler. Un route handler può tenere aperta una risposta e trasmettere eventi alla pagina in streaming. È più vicino al tempo reale, ma ogni stream aperto è una funzione ancora in esecuzione, ogni piattaforma interrompe le funzioni dopo un tempo massimo, e lo stream vive in una sola istanza, quindi un evento inviato da un’altra non lo raggiunge mai.
Un custom server. Next.js può girare dietro un server Node.js tutto tuo, con accanto una libreria websocket. Così l’app esce dall’hosting serverless: gestisci un server sempre in esecuzione, lo tieni in piedi durante i deploy e, appena ne hai più di uno, condividi ogni evento tra di loro, di solito tramite Redis, e tieni ogni browser su un server che lo conosce.
Un server websocket tutto tuo accanto all’app. L’app resta serverless e un server separato tiene le connessioni. È la più pulita delle quattro soluzioni, ed è comunque un secondo sistema da gestire: il suo certificato, la sua scalabilità, il suo monitoraggio, e un modo per sapere chi è l’utente, dato che non può leggere la sessione della tua app.
Oppure lascia che sia un cloud in tempo reale a tenere le connessioni
Un servizio in tempo reale gestito si fa carico per intero della parte che deve restare sempre attiva. I browser tengono le loro connessioni aperte con il servizio. Il tuo codice Next.js, in una server action o in un route handler, invia ogni evento con una breve richiesta HTTP, che è esattamente ciò che una funzione serverless sa fare bene. Il servizio lo consegna a ogni pagina che deve riceverlo, da qualunque istanza sia partito.
Ciò che conta è che si adatti al modo in cui Next.js funziona già: il tuo server decide chi può accedere a cosa, le tue pagine si iscrivono dai client component e nel tuo hosting non cambia nulla.
Next.js su WordPlus Cloud
Con WordPlus Cloud, il tuo server dichiara le sue stanze una volta sola, con una regola su chi può entrare in ciascuna:
import { createRealtime } from '@wordplus/realtime/server';
import { currentUserId, profileOf } from '@/lib/auth';
export const realtime = createRealtime( {
site: process.env.WORDPLUS_SITE!,
secret: process.env.WORDPLUS_SECRET!,
localKey: process.env.WORDPLUS_LOCAL_KEY!,
user: () => currentUserId(),
profile: ( user ) => profileOf( user ),
} );
// The room of an order, order:1042: its customer may join, and the last 20 updates are kept.
realtime.room( 'order:{id}', { history: 20 }, async ( user, { id } ) => user !== null && ( await ownerOf( id ) ) === user );Un unico route handler catch-all risponde alle pagine quando chiedono di entrare e serve i file dell’SDK per il browser:
import { realtime } from '@/lib/realtime';
export const GET = realtime.route;
export const POST = realtime.route;Il layout delle tue pagine per utenti autenticati renderizza RealtimeProvider con realtime.config() per l’utente, così ogni pagina al suo interno condivide un’unica connessione, mantenuta durante le navigazioni lato client. Poi un client component entra in una stanza con un hook:
'use client';
import { useState } from 'react';
import { useRoom } from '@wordplus/realtime/react';
export function Status( { id, initial }: { id: string; initial: string } ) {
const [ status, setStatus ] = useState( initial );
useRoom( `order:${ id }`, { updated: ( data: { status: string } ) => setStatus( data.status ) } );
return <p>{ status }</p>;
}E una server action invia, dopo aver salvato la modifica:
'use server';
import { realtime } from '@/lib/realtime';
export async function markShipped( id: string ) {
await saveStatus( id, 'shipped' );
await realtime.to( `order:${ id }` ).emit( 'updated', { status: 'shipped' } );
}Questo è l’intero ciclo, e gira su hosting serverless così com’è. In più:
- L’autenticazione resta la tua. L’utente è l’id con cui la tua app lo conosce, letto dentro il route handler, quindi lì funzionano la tua sessione e i tuoi cookie.
- Nulla si perde alla riconnessione. Una stanza conserva i suoi ultimi eventi, così una pagina che torna riceve ciò che si è persa, in ordine.
- Una sola connessione per tutte le schede. Le schede di un browser condividono un’unica connessione, così una persona con tre schede aperte conta una volta sola.
- Sigillato lungo il tragitto. Ciò che la tua app invia viaggia cifrato con chiavi che hanno solo il tuo server e i tuoi utenti.
- Non solo eventi. Chi è online, chiamate che squillano in ogni scheda, chiamate di gruppo e live streaming arrivano sulla stessa connessione, con la schermata di chiamata, la chat e il player pronti all’uso di Better Messages, oppure con i tuoi componenti sulla stessa API.
La guida per Next.js contiene tutta la configurazione, e la demo dal vivo è un piccolo negozio costruito su di essa, dove un ordine passa a spedito in un’altra finestra mentre fai clic.
Le videochiamate in un’app Next.js, passo dopo passo: Come implementare le videochiamate in Next.js.
WordPlus Cloud for Apps ha un prezzo in base alle connessioni simultanee, a partire da 1.000, con fatturazione mensile o annuale. Scopri cosa fa per le app, o i prezzi per le app. Domande sulla tua integrazione? Scrivici.