Aller au contenu principal

Journal de bord · Phase 0 · 2/7

Octane, FrankenPHP et Inertia SSR sur aubia.dev

Octane, FrankenPHP et Inertia SSR servent aubia.dev. Leur fonctionnement, les limites du rendu serveur et les réserves sur les chiffres de latence publiés.

Publié le 20 juillet 2026Mis à jour le 7 septembre 20268 min de lecture
  • laravel
  • octane
  • frankenphp
  • inertia
  • vite

Sur aubia.dev, Laravel reste chargé en mémoire entre les requêtes et React produit le contenu des pages côté serveur. Le navigateur reçoit ce HTML, puis le JavaScript ajoute l'interactivité.

J'ai choisi Laravel Octane avec FrankenPHP pour exécuter l'application, et le SSR d'Inertia pour rendre les pages React. Je veux que les moteurs de recherche puissent lire le contenu sans devoir exécuter le JavaScript du site.

Les chiffres de latence publiés le 25 août 2026 donnent deux indications : 75 à 280 ms à chaud, et 133 requêtes sous 615 ms après une période sans trafic. Faute de relevé et de protocole publics permettant de les vérifier, ils ne prouvent ni un gain face à PHP-FPM, ni la durée d'un réveil.

Le mode worker d'Octane

Avec PHP-FPM, Laravel est initialisé à chaque requête. Les processus PHP peuvent en traiter plusieurs avant d'être recyclés, selon leur configuration, dont pm.max_requests. C'est le démarrage de l'application qui se répète, sans nécessiter un nouveau processus à chaque fois.

Le schéma montre ce que PHP-FPM refait à chaque requête.

Chaque requête suivante repasse par cette initialisation.

Laravel Octane démarre Laravel une fois par worker. Ce worker traite les requêtes suivantes avec l'application déjà chargée. Les workers sont parfois recyclés ou redémarrés, lors d'un déploiement par exemple.

Sur aubia.dev, Octane utilise FrankenPHP, un serveur d'applications PHP écrit en Go qui intègre le serveur web Caddy.

La variable d'environnement suivante sélectionne ce driver dans une installation déjà configurée :

# .env : le driver actif sur aubia.dev
OCTANE_SERVER=frankenphp

Sous Octane, les requêtes passent par un seul bloc d'initialisation.

Le bloc ne réapparaît pas entre deux requêtes : l'application chargée sert les suivantes.

Cette mémoire conservée demande de surveiller ce que l'application y enregistre. Une donnée propre à un visiteur, stockée dans un singleton ou une propriété statique, peut être réutilisée pour le suivant.

Octane réinitialise l'état du framework entre les requêtes. Il ne nettoie pas automatiquement toutes les variables globales et propriétés statiques du code applicatif. Les services qui conservent des données propres à une requête doivent être limités à cette requête ou explicitement réinitialisés.

Les limites des chiffres publiés

Les chiffres ajoutés à cet article le 25 août 2026 indiquent 75 à 280 ms sur un processus chaud, déjà chargé en mémoire. Ils mentionnent aussi sept jours de journaux de production : 133 requêtes arrivées après plus de vingt minutes sans trafic, toutes servies sous 615 ms.

La configuration de production constatée le 11 juillet 2026 est une instance Laravel Cloud flex-1gb, avec hibernation active. Une période sans trafic rend donc un réveil possible, mais ne prouve pas que l'instance était en sommeil pour chacune de ces requêtes.

Sans les relevés et leur protocole, la durée rapportée reste difficile à interpréter. Un chronométrage applicatif peut exclure le réveil de l'instance, le passage par le proxy et le réseau jusqu'au navigateur. Ces nombres ne permettent donc pas de connaître le temps d'attente du visiteur.

Dans son annonce du 1er juin 2026, Laravel Cloud indique un réveil de la stack entière sous 500 ms. Le fournisseur annonce un temps vingt fois inférieur à celui de la génération précédente, avec un à quatre vCPU selon la demande.

Les chiffres d'aubia.dev ne suffisent pas à confirmer cette annonce du fournisseur. Aucun comparatif de cette vitrine avec PHP-FPM n'est disponible.

Le rendu serveur d'Inertia

Une page React rendue uniquement côté client dépend du chargement et de l'exécution du JavaScript pour afficher son contenu. Les robots ne l'exécutent pas tous, et ceux qui le font peuvent différer ce travail.

Avec le SSR d'Inertia v3, Laravel transmet la page et ses données à un processus Node. Celui-ci rend les composants React et renvoie le HTML à Laravel, qui l'intègre à la réponse avec la vue Blade.

Le contenu et les métadonnées de la page sont alors disponibles dans la réponse HTML initiale. Ce rendu prend du temps : Laravel doit attendre le résultat de Node avant d'envoyer la page.

Dans le navigateur, React hydrate le HTML reçu pour rendre les boutons, le formulaire et les autres composants interactifs.

Le schéma retrace une première visite, de la requête du navigateur à l'hydratation.

Le compteur de la liste d'attente ne fait pas partie de ce premier rendu. Il arrive par une requête suivante, décrite plus bas.

En développement, le plugin Vite d'Inertia prend en charge le SSR sans serveur de rendu à lancer séparément. En production, le processus Node reste distinct du serveur PHP et doit être exploité avec lui.

La configuration du SSR est déclarée dans config/inertia.php :

// config/inertia.php, extrait
'ssr' => [
    'enabled' => (bool) env('INERTIA_SSR_ENABLED', true),
    'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
    'url' => env('INERTIA_SSR_URL'), // adresse interne par défaut omise
    'ensure_bundle_exists' => (bool) env('INERTIA_SSR_ENSURE_BUNDLE_EXISTS', true),
    'throw_on_error' => (bool) env('INERTIA_SSR_THROW_ON_ERROR', false),
],

Avec ensure_bundle_exists, un bundle SSR absent interrompt la tentative de rendu : Inertia retourne null, sans émettre d'événement d'échec. Si une tentative de rendu échoue, Inertia émet SsrRenderFailed. Aucun listener applicatif ne traite cet événement sur la vitrine.

Le réglage throw_on_error à false permet alors le repli vers le rendu client. La vue app.blade.php conserve un titre et une description génériques, mais le contenu de la page et ses métadonnées spécifiques dépendent du JavaScript.

Je préfère ce repli à une page d'erreur en cas de panne SSR passagère. Il a un coût : l'affichage peut être retardé, et un robot qui n'exécute pas JavaScript ne reçoit plus le contenu attendu. L'absence de listener laisse aussi ces échecs sans suivi applicatif dédié.

Le code splitting avec Rolldown

Côté build, aubia.dev utilise Vite 8. Le bundler est Rolldown, écrit en Rust et intégré nativement à Vite.

Le code splitting est le découpage du JavaScript compilé en plusieurs fichiers chargés séparément. Il est réglé explicitement dans la configuration de build :

// vite.config.ts
build: {
    rolldownOptions: {
        output: {
            codeSplitting: {
                minSize: 20_000,
                groups: [
                    {
                        name: 'react',
                        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
                        priority: 50,
                    },
                    {
                        name: 'motion',
                        test: /[\\/]node_modules[\\/](motion|motion-dom|motion-utils)[\\/]/,
                        priority: 40,
                    },
                    {
                        name: 'fontawesome',
                        test: /[\\/]node_modules[\\/]@fortawesome[\\/]/,
                        priority: 30,
                    },
                    {
                        name: 'inertia',
                        test: /[\\/]node_modules[\\/]@inertiajs[\\/]/,
                        priority: 20,
                    },
                    {
                        name: 'radix',
                        test: /[\\/]node_modules[\\/]@radix-ui[\\/]/,
                        priority: 10,
                    },
                ],
            },
        },
    },
},

Cinq groupes sont déclarés : React et son scheduler, les animations Motion, les icônes FontAwesome, le client Inertia et les composants Radix. Cette séparation facilite la réutilisation du cache d'un déploiement à l'autre si les fichiers générés restent inchangés. Elle ne garantit pas que les fichiers de dépendances seront identiques après chaque build.

Le build de production ajoute un bundle SSR dédié, produit par vite build --ssr.

Le compteur après le rendu initial

Le compteur de la liste d'attente récupère son total depuis aubia.cloud, avec un cache côté serveur. Si le cache est vide, cet appel réseau peut retarder la réponse.

Le middleware HandleInertiaRequests partage donc le compteur en différé. Le navigateur demande sa valeur dans une requête supplémentaire, après le rendu initial :

// app/Http/Middleware/HandleInertiaRequests.php
'waitlist' => [
    'count' => Inertia::defer(fn(): ?int => app(FetchWaitlistCountQuery::class)->run(), rescue: true),
],

Le premier rendu n'attend pas cet appel. Si le cache lui-même tombe en panne, rescue: true omet la prop du payload sans faire échouer la requête différée. Le composant conserve alors son affichage neutre.

La requête supplémentaire du compteur part une fois la page affichée.

Laravel répond en JSON, sans passer par Node : le total vient du cache serveur ou d'aubia.cloud.

Le choix de FrankenPHP

Le fichier config/octane.php prévoit RoadRunner en l'absence de variable d'environnement :

// config/octane.php
'server' => env('OCTANE_SERVER', 'roadrunner'),

RoadRunner est un autre serveur compatible avec les workers d'Octane. Swoole est aussi disponible, mais exige une extension PHP compilée.

J'ai retenu FrankenPHP pour son mode worker et l'intégration de Caddy dans le même binaire. Sa prise en charge de HTTP/3 et des certificats automatiques figurait aussi dans la note de décision d'architecture du projet.

Ces deux dernières capacités ne sont pas utilisées sur aubia.dev : le proxy de production termine TLS et HTTP/3 en amont. Le binaire intégré simplifie la partie PHP du déploiement, mais le SSR nécessite toujours son processus Node distinct.

Un passage à RoadRunner demanderait d'installer son binaire, d'adapter la configuration et le démarrage, puis de tester le déploiement. La variable OCTANE_SERVER sélectionne le driver ; elle ne remplace pas ce travail.

Octane évite de réinitialiser Laravel à chaque requête, et le SSR fournit le contenu React dans la réponse HTML initiale. Les chiffres publiés restent des indications historiques : sans relevés vérifiables ni protocole, ils ne démontrent ni un gain face à PHP-FPM, ni le temps d'un réveil complet.

Ce chantier se raconte ici, article après article. Ce qui vient ensuite dépend de ce que vous en direz.

Rejoindre la liste d'attente