Blog

Des websockets dans Next.js : le temps réel sur un hébergement serverless

· Andrij Tkachenko · 6 min de lecture

Trois fonctions serveur éphémères, chacune avec un chronomètre, reliées à un nuage qui garde des connexions en direct ouvertes avec quatre fenêtres de navigateur

Ajouter des mises à jour en direct à une application Next.js, cela commence en général bien. Vous ouvrez un websocket depuis un client component, vous le faites pointer vers un petit serveur, et avec next dev tout fonctionne : la commande passe à « expédiée », le nouveau message apparaît, le compteur des personnes en ligne bouge. Puis vous déployez, et la connexion tombe, se reconnecte, retombe, ou ne s’ouvre jamais.

Le code est rarement en cause. La plupart des applications Next.js tournent sur un hébergement serverless, et une fonction serverless ne peut pas garder une connexion ouverte. Cet article explique pourquoi, ce que coûtent les contournements habituels, et comment ajouter le temps réel à une application Next.js sans faire tourner de serveur websocket.

Pourquoi une fonction serverless ne peut pas garder un websocket

Un route handler, une server action et un server component fonctionnent tous de la même façon sur un hébergement serverless. Une requête arrive, une fonction démarre ou se réveille, elle répond, et peu après elle est gelée ou arrêtée. La requête suivante peut très bien tomber sur une tout autre instance.

Un websocket demande l’inverse : un seul processus qui garde une connexion ouverte avec chaque navigateur aussi longtemps que la page reste ouverte, souvent des heures, et qui peut lui envoyer quelque chose à tout moment. Une fonction serverless ne vit pas aussi longtemps, n’a aucun moyen d’atteindre une connexion ouverte par une autre instance, et elle est facturée pour chaque seconde où elle reste active. C’est pourquoi tant de premières tentatives fonctionnent en développement, où un seul processus qui tourne en continu sert tout, puis échouent en production.

Les contournements habituels

Le polling. Toutes les quelques secondes, la page demande à un route handler si quelque chose a changé. Cela fonctionne partout, mais chaque question est un appel de fonction, dont la plupart ne rapportent rien de nouveau, et la page a toujours quelques secondes de retard. Avec 1 000 personnes sur une page qui pose la question toutes les 5 secondes, cela fait 200 appels de fonction par seconde, jour et nuit.

Le streaming depuis un route handler. Un route handler peut garder une réponse ouverte et envoyer des événements en continu à la page. C’est plus proche du direct, mais chaque flux ouvert est une fonction qui tourne encore, chaque plateforme coupe les fonctions au bout d’une durée maximale, et le flux vit dans une seule instance, si bien qu’un événement envoyé depuis une autre ne l’atteint jamais.

Un custom server. Next.js peut tourner derrière votre propre serveur Node.js, avec une bibliothèque websocket à côté. L’application quitte alors l’hébergement serverless : vous faites tourner un serveur en continu, vous le maintenez en marche pendant les déploiements, et dès que vous en avez plus d’un, vous partagez chaque événement entre eux, en général par Redis, et vous gardez chaque navigateur sur un serveur qui le connaît.

Votre propre serveur websocket à côté de l’application. L’application reste serverless et un serveur distinct tient les connexions. C’est la plus propre des quatre solutions, et c’est tout de même un second système à faire tourner : son certificat, sa montée en charge, sa surveillance, et un moyen de savoir qui est l’utilisateur, puisqu’il ne peut pas lire la session de votre application.

Ou confier les connexions à un cloud temps réel

Un service temps réel hébergé vous débarrasse entièrement de la partie qui doit durer. Les navigateurs gardent leurs connexions ouvertes avec le service. Votre code Next.js, dans une server action ou un route handler, envoie chaque événement par une seule courte requête HTTP, exactement ce pour quoi une fonction serverless est faite. Le service le distribue à chaque page qui doit le recevoir, quelle que soit l’instance qui l’a envoyé.

L’important, c’est qu’il s’accorde avec la façon dont Next.js fonctionne déjà : votre serveur décide qui peut rejoindre quoi, vos pages s’abonnent depuis des client components, et rien ne change dans votre hébergement.

Next.js sur WordPlus Cloud

Avec WordPlus Cloud, votre serveur déclare ses salons une fois pour toutes, avec pour chacun une règle qui dit qui peut le rejoindre :

lib/realtime.ts
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 seul route handler catch-all répond aux pages quand elles demandent à rejoindre un salon, et sert les fichiers du SDK destinés au navigateur :

app/api/realtime/[...path]/route.ts
import { realtime } from '@/lib/realtime';

export const GET = realtime.route;
export const POST = realtime.route;

Le layout de vos pages réservées aux utilisateurs connectés rend RealtimeProvider avec realtime.config() pour l’utilisateur, si bien que chaque page en dessous partage une seule connexion, conservée lors des navigations côté client. Un client component rejoint ensuite un salon avec un hook :

app/(app)/orders/[id]/status.tsx
'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>;
}

Et une server action envoie, une fois la modification enregistrée :

app/(app)/orders/[id]/actions.ts
'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' } );
}

Voilà toute la boucle, et elle tourne telle quelle sur un hébergement serverless. Au-delà :

  • Votre authentification reste la vôtre. L’utilisateur est l’identifiant, quel qu’il soit, sous lequel votre application le connaît, lu dans le route handler, si bien que votre session et vos cookies y fonctionnent.
  • Rien de perdu à la reconnexion. Un salon garde ses derniers événements, si bien qu’une page qui revient reçoit ce qu’elle a manqué, dans l’ordre.
  • Une seule connexion pour tous les onglets. Les onglets d’un même navigateur partagent une seule connexion, si bien qu’une personne avec trois onglets ouverts ne compte qu’une fois.
  • Scellé en chemin. Ce que votre application envoie voyage chiffré, avec des clés que seuls votre serveur et vos utilisateurs détiennent.
  • Plus que des événements. La liste des personnes en ligne, les appels qui sonnent dans chaque onglet, les appels de groupe et les diffusions en direct passent par la même connexion, avec un écran d’appel, un chat et un lecteur prêts à l’emploi venus de Better Messages, ou vos propres composants sur la même API.

Le guide Next.js détaille toute la mise en place, et la démo en ligne est une petite boutique construite dessus, où une commande passe à « expédiée » dans une autre fenêtre au moment où vous cliquez.

Les appels vidéo dans une app Next.js, pas à pas : Comment mettre en place des appels vidéo dans Next.js.

WordPlus Cloud for Apps a un prix selon les connexions simultanées, à partir de 1 000, facturé au mois ou à l’année. Découvrez ce qu’il apporte aux applications, ou consultez les tarifs pour les applications. Des questions sur votre intégration ? Écrivez-nous.