WebSockets in Next.js: Echtzeit auf Serverless-Hosting

Live-Updates in eine Next.js-App einzubauen, beginnt meist gut. Sie öffnen einen WebSocket aus einer Client Component, richten ihn auf einen kleinen Server, und mit next dev funktioniert alles: Die Bestellung wechselt auf „versendet“, die neue Nachricht erscheint, die Anzeige, wie viele gerade online sind, ändert sich. Dann deployen Sie, und die Verbindung bricht ab, verbindet sich neu, bricht wieder ab oder öffnet sich gar nicht erst.
Am Code liegt es selten. Die meisten Next.js-Apps laufen auf Serverless-Hosting, und eine Serverless-Funktion kann keine Verbindung offen halten. Dieser Beitrag erklärt, warum, was die üblichen Workarounds kosten und wie Sie einer Next.js-App Echtzeit geben, ohne einen WebSocket-Server zu betreiben.
Warum eine Serverless-Funktion keinen WebSocket halten kann
Ein Route Handler, eine Server Action und eine Server Component funktionieren auf einem Serverless-Host alle gleich. Eine Anfrage kommt herein, eine Funktion startet oder wacht auf, sie antwortet, und kurz danach wird sie eingefroren oder beendet. Die nächste Anfrage landet womöglich auf einer ganz anderen Instanz.
Ein WebSocket braucht das Gegenteil: einen Prozess, der eine Verbindung zu jedem Browser so lange offen hält, wie die Seite offen ist, oft stundenlang, und jederzeit etwas an ihn senden kann. Eine Serverless-Funktion lebt nicht so lange, kann eine Verbindung, die eine andere Instanz geöffnet hat, nicht erreichen und wird für jede Sekunde abgerechnet, die sie läuft. Deshalb funktionieren so viele erste Versuche in der Entwicklung, wo ein einziger, dauerhaft laufender Prozess alles bedient, und scheitern in Produktion.
Die üblichen Workarounds
Polling. Die Seite fragt alle paar Sekunden bei einem Route Handler nach, ob sich etwas geändert hat. Das funktioniert überall, aber jede Frage ist ein Funktionsaufruf, die meisten davon werden mit nichts Neuem beantwortet, und die Seite hinkt immer ein paar Sekunden hinterher. Bei 1.000 Personen auf einer Seite, die alle 5 Sekunden fragt, sind das rund um die Uhr 200 Funktionsaufrufe pro Sekunde.
Streaming aus einem Route Handler. Ein Route Handler kann eine Antwort offen halten und Events an die Seite streamen. Das kommt Echtzeit näher, aber jeder offene Stream ist eine Funktion, die noch läuft, jede Plattform beendet Funktionen nach einer Höchstdauer, und der Stream lebt in einer einzigen Instanz, sodass ein Event, das von einer anderen gesendet wird, ihn nie erreicht.
Ein Custom Server. Next.js kann hinter einem eigenen Node.js-Server laufen, mit einer WebSocket-Bibliothek daneben. Damit verlässt die App das Serverless-Hosting: Sie betreiben einen dauerhaft laufenden Server, halten ihn über Deploys hinweg am Laufen, und sobald Sie mehr als einen haben, verteilen Sie jedes Event zwischen ihnen, meist über Redis, und sorgen dafür, dass jeder Browser bei einem Server bleibt, der ihn kennt.
Ein eigener WebSocket-Server neben der App. Die App bleibt serverless, und ein separater Server hält die Verbindungen. Das ist der sauberste der vier Wege, und trotzdem ist es ein zweites System, das Sie betreiben müssen: sein Zertifikat, seine Skalierung, seine Überwachung und ein Weg, herauszufinden, wer der Nutzer ist, denn er kann die Session Ihrer App nicht lesen.
Oder eine Echtzeit-Cloud hält die Verbindungen
Ein gehosteter Echtzeit-Dienst nimmt Ihnen den dauerhaft laufenden Teil ganz ab. Die Browser halten ihre Verbindungen zum Dienst offen. Ihr Next.js-Code sendet jedes Event aus einer Server Action oder einem Route Handler in einer kurzen HTTP-Anfrage, und genau darin ist eine Serverless-Funktion gut. Der Dienst stellt es jeder Seite zu, die es bekommen soll, ganz gleich, welche Instanz es gesendet hat.
Entscheidend ist, dass er zu der Art passt, wie Next.js ohnehin arbeitet: Ihr Server legt fest, wer wo beitreten darf, Ihre Seiten abonnieren aus Client Components, und an Ihrem Hosting ändert sich nichts.
Next.js auf WordPlus Cloud
Mit WordPlus Cloud legt Ihr Server seine Räume einmal fest, jeweils mit einer Regel dafür, wer beitreten darf:
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 );Ein Route Handler als Catch-all beantwortet die Anfragen der Seiten, die beitreten wollen, und liefert die Browserdateien des SDK aus:
import { realtime } from '@/lib/realtime';
export const GET = realtime.route;
export const POST = realtime.route;Das Layout Ihrer Seiten für angemeldete Nutzer rendert RealtimeProvider mit realtime.config() für den Nutzer, sodass sich jede Seite darunter eine Verbindung teilt, die beim Navigieren im Client erhalten bleibt. Eine Client Component tritt dann mit einem Hook einem Raum bei:
'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>;
}Und eine Server Action sendet, nachdem die Änderung gespeichert ist:
'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' } );
}Damit ist der Kreis geschlossen, und das Ganze läuft unverändert auf Serverless-Hosting. Darüber hinaus:
- Ihre Anmeldung bleibt bei Ihnen. Der Nutzer ist die ID, unter der Ihre App ihn kennt, ausgelesen im Route Handler, sodass Ihre Session und Ihre Cookies dort funktionieren.
- Nichts geht bei einer Neuverbindung verloren. Ein Raum bewahrt seine letzten Events auf, sodass eine Seite, die zurückkommt, bekommt, was sie verpasst hat, in der richtigen Reihenfolge.
- Eine Verbindung für alle Tabs. Die Tabs eines Browsers teilen sich eine Verbindung, sodass eine Person mit drei offenen Tabs einmal zählt.
- Unterwegs versiegelt. Was Ihre App sendet, wird verschlüsselt übertragen, mit Schlüsseln, die nur Ihr Server und Ihre Nutzer haben.
- Mehr als Events. Online-Status, Anrufe, die in jedem Tab klingeln, Gruppenanrufe und Livestreams laufen über dieselbe Verbindung, mit fertigem Anrufbildschirm, Chat und Player aus Better Messages oder mit Ihren eigenen Komponenten auf derselben API.
Die Next.js-Anleitung enthält die ganze Einrichtung, und die Live-Demo ist ein kleiner Shop, der darauf aufbaut und in dem eine Bestellung in einem anderen Fenster auf „versendet“ wechselt, während Sie klicken.
Videoanrufe in einer Next.js-App, Schritt für Schritt: So implementieren Sie Videoanrufe in Next.js.
Für WordPlus Cloud for Apps gelten Preise nach gleichzeitigen Verbindungen, ab 1.000, monatlich oder jährlich abgerechnet. Sehen Sie, was sie für Apps leistet, oder werfen Sie einen Blick auf die Preise für Apps. Fragen zu Ihrer Integration? Schreiben Sie uns.