Vai al contenuto principale

Diario di bordo · Fase 0 · 2/7

Octane, FrankenPHP e Inertia SSR su aubia.dev

Octane, FrankenPHP e Inertia SSR su aubia.dev: il funzionamento, i limiti del rendering lato server e le cautele sui dati di latenza pubblicati.

Pubblicato il 20 luglio 2026Aggiornato il 7 settembre 20267 min di lettura
  • laravel
  • octane
  • frankenphp
  • inertia
  • vite

Su aubia.dev, Laravel resta caricato in memoria tra le richieste e React genera il contenuto delle pagine sul server. Il browser riceve quell'HTML, poi JavaScript aggiunge l'interattività.

Ho scelto Laravel Octane con FrankenPHP per eseguire l'applicazione e l'SSR di Inertia per generare le pagine React. Voglio che i motori di ricerca possano leggere il contenuto senza dover eseguire il JavaScript del sito.

I dati di latenza pubblicati il 25 agosto 2026 riportano due risultati: da 75 a 280 ms a caldo e 133 richieste sotto i 615 ms dopo un periodo senza traffico. Senza registrazioni pubbliche e un protocollo di misurazione che permettano di verificarli, non dimostrano né un miglioramento rispetto a PHP-FPM né un tempo di risveglio.

La modalità worker di Octane

Con PHP-FPM, Laravel viene inizializzato a ogni richiesta. I processi PHP possono gestire più richieste prima di essere riciclati, in base alla configurazione, compresa l'impostazione pm.max_requests. L'avvio dell'applicazione si ripete senza richiedere ogni volta un nuovo processo.

Il diagramma mostra ciò che PHP-FPM ripete a ogni richiesta.

Ogni richiesta successiva ripassa da questa inizializzazione.

Laravel Octane avvia Laravel una volta per worker. Quel worker gestisce le richieste successive con l'applicazione già caricata. I worker vengono riciclati o riavviati di tanto in tanto, per esempio durante un deploy.

Su aubia.dev, Octane usa FrankenPHP, un server applicativo PHP scritto in Go che integra il server web Caddy.

La seguente variabile d'ambiente seleziona questo driver in un'installazione già configurata:

# .env: il driver attivo su aubia.dev
OCTANE_SERVER=frankenphp

Con Octane, le richieste passano da un solo blocco di inizializzazione.

Il blocco non ricompare tra due richieste: l'applicazione caricata serve le successive.

Mantenere l'applicazione in memoria richiede attenzione a ciò che vi conserva. I dati di un visitatore, memorizzati in un singleton o in una proprietà statica, possono essere riutilizzati per il successivo.

Octane ripristina lo stato del framework tra le richieste. Non svuota automaticamente ogni variabile globale e proprietà statica del codice applicativo. I servizi che conservano dati specifici di una richiesta devono avere uno scope limitato a quella richiesta oppure essere azzerati esplicitamente.

I limiti dei dati pubblicati

I dati aggiunti a questo articolo il 25 agosto 2026 riportano da 75 a 280 ms su un processo caldo, già caricato in memoria. Fanno anche riferimento a sette giorni di log di produzione: 133 richieste arrivate dopo più di venti minuti senza traffico, tutte servite in meno di 615 ms.

La configurazione di produzione osservata l'11 luglio 2026 era un'istanza Laravel Cloud flex-1gb con ibernazione abilitata. Un periodo senza traffico rende quindi possibile un risveglio, ma non dimostra che l'istanza fosse in ibernazione per ciascuna di quelle richieste.

Senza le registrazioni e il relativo protocollo di misurazione, la durata riportata resta difficile da interpretare. Una misura a livello applicativo può escludere il risveglio dell'istanza, l'elaborazione del proxy e il percorso di rete fino al browser. Questi numeri non permettono quindi di sapere quanto attende un visitatore.

Nel suo annuncio del 1° giugno 2026, Laravel Cloud riporta un tempo di risveglio inferiore a 500 ms per l'intero stack. Il fornitore dichiara una riduzione di venti volte rispetto alla generazione precedente, con un numero di vCPU da una a quattro in base alla domanda.

I dati di aubia.dev non bastano a confermare la dichiarazione del fornitore. Non è disponibile alcun confronto di questo sito con PHP-FPM.

Il rendering lato server di Inertia

Una pagina React generata solo sul client dipende dal caricamento e dall'esecuzione di JavaScript prima che il contenuto appaia. Non tutti i crawler lo eseguono e quelli che lo fanno possono rimandarne l'esecuzione.

Con l'SSR di Inertia v3, Laravel invia la pagina e i suoi dati a un processo Node. Node esegue il rendering dei componenti React e restituisce HTML a Laravel, che lo include nella risposta tramite la vista Blade.

Il contenuto della pagina e i metadati sono così disponibili nella risposta HTML iniziale. Il rendering richiede tempo: Laravel deve attendere il risultato di Node prima di inviare la pagina.

Nel browser, React idrata l'HTML ricevuto per rendere interattivi i pulsanti, il modulo e gli altri componenti.

Il diagramma ripercorre una prima visita, dalla richiesta del browser all'idratazione.

Il contatore della lista d'attesa non fa parte di questo primo rendering. Arriva con una richiesta successiva, descritta più avanti.

In sviluppo, il plugin Vite di Inertia gestisce l'SSR senza richiedere l'avvio di un server di rendering separato. In produzione, il processo Node resta separato dal server PHP e va gestito insieme a esso.

La configurazione dell'SSR è dichiarata in config/inertia.php:

// config/inertia.php, estratto
'ssr' => [
    'enabled' => (bool) env('INERTIA_SSR_ENABLED', true),
    'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
    'url' => env('INERTIA_SSR_URL'), // indirizzo interno predefinito omesso
    'ensure_bundle_exists' => (bool) env('INERTIA_SSR_ENSURE_BUNDLE_EXISTS', true),
    'throw_on_error' => (bool) env('INERTIA_SSR_THROW_ON_ERROR', false),
],

Con ensure_bundle_exists, l'assenza del bundle SSR interrompe il tentativo di rendering: Inertia restituisce null senza emettere un evento di errore. Se un tentativo di rendering fallisce, Inertia emette SsrRenderFailed. Su questo sito nessun listener applicativo gestisce quell'evento.

L'impostazione throw_on_error a false consente allora il ripiego sul rendering lato client. La vista app.blade.php conserva un titolo e una descrizione generici, ma il contenuto della pagina e i suoi metadati specifici dipendono da JavaScript.

Preferisco questo ripiego a una pagina di errore durante un guasto transitorio dell'SSR. Ha un costo: il contenuto può apparire più tardi e un crawler che non esegue JavaScript non riceve più il contenuto previsto. Senza un listener, manca anche un monitoraggio applicativo dedicato a questi errori.

Il code splitting con Rolldown

Sul fronte build, aubia.dev usa Vite 8. Il bundler è Rolldown, scritto in Rust e integrato nativamente in Vite.

Il code splitting suddivide il JavaScript compilato in più file che si caricano separatamente. È configurato esplicitamente nelle impostazioni di 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,
                    },
                ],
            },
        },
    },
},

Sono dichiarati cinque gruppi: React e il suo scheduler, le animazioni Motion, le icone FontAwesome, il client Inertia e i componenti Radix. Questa separazione facilita il riutilizzo dei file in cache tra i deploy, se i file generati restano invariati. Non garantisce che i file delle dipendenze siano identici dopo ogni build.

La build di produzione aggiunge un bundle SSR dedicato, prodotto da vite build --ssr.

Il contatore dopo il rendering iniziale

Il contatore della lista d'attesa recupera il totale da aubia.cloud, con una cache lato server. Se la cache è vuota, quella chiamata di rete può ritardare la risposta.

Il middleware HandleInertiaRequests condivide quindi il contatore come prop differita. Il browser ne richiede il valore con una richiesta aggiuntiva dopo il rendering iniziale:

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

Il rendering iniziale non attende questa chiamata. Se è la cache stessa a non funzionare, rescue: true omette la prop dal payload senza far fallire la richiesta differita. Il componente mantiene allora la visualizzazione neutra.

La richiesta aggiuntiva del contatore parte una volta visualizzata la pagina.

Laravel risponde in JSON, senza passare da Node: il totale proviene dalla cache del server o da aubia.cloud.

La scelta di FrankenPHP

Il file config/octane.php usa RoadRunner come valore predefinito quando non è impostata alcuna variabile d'ambiente:

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

RoadRunner è un altro server compatibile con i worker di Octane. È disponibile anche Swoole, ma richiede un'estensione PHP compilata.

Ho scelto FrankenPHP per la modalità worker e l'integrazione di Caddy nello stesso binario. Anche il supporto per HTTP/3 e i certificati automatici figurava nel documento di decisione architetturale del progetto.

Queste ultime due funzionalità non vengono usate su aubia.dev: il proxy di produzione termina TLS e HTTP/3 a monte. Il binario integrato semplifica la parte PHP del deploy, ma l'SSR richiede comunque un processo Node separato.

Passare a RoadRunner richiederebbe l'installazione del suo binario, l'adattamento della configurazione e dell'avvio, poi i test del deploy. La variabile OCTANE_SERVER seleziona il driver; non sostituisce quel lavoro.

Octane evita di reinizializzare Laravel a ogni richiesta e l'SSR fornisce il contenuto React nella risposta HTML iniziale. I dati pubblicati restano riferimenti storici: senza registrazioni verificabili e un protocollo di misurazione, non dimostrano né un miglioramento rispetto a PHP-FPM né il tempo completo di risveglio.

Questo cantiere si racconta qui, articolo dopo articolo. Ciò che viene dopo dipende da ciò che lei ne dirà.

Si iscriva alla lista d'attesa