Websockets in Next.js: realtime op serverless hosting

Live updates toevoegen aan een Next.js-app begint meestal goed. Je opent een websocket vanuit een client component, laat hem verbinden met een kleine server, en met next dev werkt alles: de bestelling krijgt de status verzonden, het nieuwe bericht verschijnt, de teller van wie er online is verandert. Dan deploy je, en de verbinding valt weg, komt terug, valt weer weg, of gaat helemaal nooit open.
De code is zelden het probleem. De meeste Next.js-apps draaien op serverless hosting, en een serverless functie kan geen verbinding openhouden. In dit artikel lees je waarom, wat de gebruikelijke workarounds kosten, en hoe je realtime aan een Next.js-app toevoegt zonder een websocketserver te draaien.
Waarom een serverless functie geen websocket kan openhouden
Een route handler, een server action en een server component werken op een serverless host allemaal op dezelfde manier. Er komt een verzoek binnen, een functie start of wordt wakker, geeft antwoord, en kort daarna wordt ze bevroren of gestopt. Het volgende verzoek kan op een heel andere instance terechtkomen.
Een websocket vraagt het omgekeerde: één proces dat de verbinding met elke browser openhoudt zolang de pagina open is, vaak urenlang, en er op elk moment iets naartoe kan pushen. Een serverless functie leeft niet zo lang, kan op geen enkele manier bij een verbinding die een andere instance heeft geopend, en wordt gefactureerd voor elke seconde dat ze draait. Daarom werken zoveel eerste pogingen in de ontwikkelomgeving, waar één proces dat blijft draaien alles afhandelt, en gaan ze mis in productie.
De gebruikelijke workarounds
Polling. De pagina vraagt om de paar seconden aan een route handler of er iets veranderd is. Dat werkt overal, maar elke vraag is een functieaanroep, de meeste krijgen niets nieuws als antwoord, en de pagina loopt altijd een paar seconden achter. Met 1.000 mensen op een pagina die elke 5 seconden pollt, zijn dat 200 functieaanroepen per seconde, de klok rond.
Streamen vanuit een route handler. Een route handler kan een response openhouden en events naar de pagina streamen. Dat komt dichter bij live, maar elke open stream is een functie die nog draait, elk platform breekt functies af na een maximale tijd, en de stream leeft in één instance, dus een event dat vanuit een andere instance wordt verstuurd, bereikt hem nooit.
Een custom server. Next.js kan achter een eigen Node.js-server draaien, met een websocketbibliotheek ernaast. Daarmee haal je de app van serverless hosting af: je draait een server die continu aan staat, houdt hem in de lucht tijdens deploys, en zodra je er meer dan één hebt, deel je elk event onderling, meestal via Redis, en zorg je dat elke browser bij een server blijft die hem kent.
Een eigen websocketserver naast de app. De app blijft serverless en een aparte server houdt de verbindingen vast. Het is de netste van de vier, en toch een tweede systeem om te draaien: met een eigen certificaat, eigen schaling en eigen monitoring, en een manier om te weten wie de gebruiker is, want de sessie van je app kan hij niet lezen.
Of laat een realtime cloud de verbindingen vasthouden
Een gehoste realtime dienst neemt je het langdurige deel volledig uit handen. De browsers houden hun verbindingen open met de dienst. Je Next.js-code, in een server action of een route handler, verstuurt elk event in één kort HTTP-verzoek, en daar is een serverless functie juist goed in. De dienst bezorgt het aan elke pagina die het moet krijgen, welke instance het ook verstuurde.
Waar het om gaat, is dat het past bij hoe Next.js al werkt: je server bepaalt wie waaraan mag deelnemen, je pagina’s abonneren zich vanuit client components, en aan je hosting verandert niets.
Next.js op WordPlus Cloud
Met WordPlus Cloud legt je server zijn ruimtes één keer vast, elk met een regel voor wie erin mag:
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 );Eén catch-all route handler beantwoordt de pagina’s als ze vragen om deel te nemen, en serveert de browserbestanden van de SDK:
import { realtime } from '@/lib/realtime';
export const GET = realtime.route;
export const POST = realtime.route;De layout van je pagina’s achter de login rendert RealtimeProvider met realtime.config() voor de gebruiker, zodat elke pagina eronder één verbinding deelt, die bij client-side navigatie blijft bestaan. Een client component neemt dan met een hook deel aan een ruimte:
'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>;
}En een server action verstuurt, nadat de wijziging is opgeslagen:
'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' } );
}Daarmee is de cirkel rond, en het draait zonder aanpassingen op serverless hosting. En daarnaast:
- Je login blijft van jou. De gebruiker is welk id je app ook gebruikt, uitgelezen in de route handler, zodat je sessie en cookies daar werken.
- Niets kwijt bij opnieuw verbinden. Een ruimte bewaart de laatste events, dus een pagina die terugkomt, krijgt wat ze gemist heeft, in de juiste volgorde.
- Eén verbinding voor alle tabbladen. De tabbladen van een browser delen één verbinding, zodat iemand met drie tabbladen open maar één keer telt.
- Verzegeld onderweg. Wat je app verstuurt, reist versleuteld met sleutels die alleen je server en je gebruikers hebben.
- Meer dan events. Wie er online is, gesprekken die in elk tabblad overgaan, groepsgesprekken en livestreams lopen over dezelfde verbinding, met een kant-en-klaar belscherm, chat en speler van Better Messages, of je eigen componenten op dezelfde API.
De Next.js-handleiding beschrijft de hele installatie, en de live demo is een kleine webwinkel die erop draait, waar een bestelling in een ander venster de status verzonden krijgt terwijl je klikt.
Videogesprekken in een Next.js-app, stap voor stap: Zo bouw je videogesprekken in Next.js.
WordPlus Cloud for Apps is geprijsd op gelijktijdige verbindingen, vanaf 1.000, maandelijks of jaarlijks gefactureerd. Bekijk wat het doet voor apps, of de prijzen voor apps. Vragen over je integratie? Schrijf ons.