Devlog · Phase 0 · 2/7
Octane, FrankenPHP und Inertia SSR auf aubia.dev
Octane, FrankenPHP und Inertia SSR betreiben aubia.dev. Ihre Funktionsweise, die Grenzen des Server-Renderings und die Aussagekraft der veröffentlichten Latenzwerte.
- laravel
- octane
- frankenphp
- inertia
- vite

Auf aubia.dev bleibt Laravel zwischen den Anfragen im Speicher, und React rendert die Seiteninhalte auf dem Server. Der Browser erhält dieses HTML, anschließend ergänzt JavaScript die Interaktivität.
Ich habe Laravel Octane mit FrankenPHP für den Anwendungsbetrieb und Inertias SSR für das Rendering der React-Seiten gewählt. Suchmaschinen sollen die Inhalte lesen können, ohne das JavaScript der Website ausführen zu müssen.
Die am 25. August 2026 veröffentlichten Latenzwerte nennen zwei Ergebnisse: 75 bis 280 ms im warmen Zustand und 133 Anfragen unter 615 ms nach einer Zeit ohne Traffic. Ohne öffentliche Aufzeichnungen und ein Messprotokoll zur Überprüfung belegen sie weder einen Vorteil gegenüber PHP-FPM noch eine Aufwachzeit.
Der Worker-Modus von Octane
Mit PHP-FPM wird Laravel für jede Anfrage initialisiert. PHP-Prozesse können vor ihrer Erneuerung mehrere Anfragen bearbeiten, abhängig von ihrer Konfiguration, darunter pm.max_requests. Der Anwendungsstart wiederholt sich, ohne dass dafür jedes Mal ein neuer Prozess nötig ist.
Das Diagramm zeigt, was PHP-FPM bei jeder Anfrage wiederholt.
Jede folgende Anfrage durchläuft diese Initialisierung erneut.
Laravel Octane startet Laravel einmal pro Worker. Dieser bearbeitet die folgenden Anfragen mit der bereits geladenen Anwendung. Worker werden gelegentlich erneuert oder neu gestartet, etwa bei einem Deployment.
Auf aubia.dev nutzt Octane FrankenPHP, einen in Go geschriebenen PHP-Anwendungsserver mit eingebettetem Caddy-Webserver.
Die folgende Umgebungsvariable wählt diesen Treiber in einer bereits konfigurierten Installation aus:
# .env: der aktive Treiber auf aubia.dev
OCTANE_SERVER=frankenphp
Unter Octane laufen die Anfragen durch einen einzigen Initialisierungsblock.
Der Block erscheint zwischen zwei Anfragen nicht erneut: Die geladene Anwendung bedient die folgenden.
Bleibt die Anwendung im Speicher, muss klar sein, welche Daten sie dort aufbewahrt. Besucherspezifische Daten in einem Singleton oder einer statischen Eigenschaft können bei der nächsten Anfrage wiederverwendet werden.
Octane setzt den Framework-Zustand zwischen Anfragen zurück. Es löscht nicht automatisch jede globale Variable und statische Eigenschaft im Anwendungscode. Dienste mit anfragespezifischen Daten müssen auf die jeweilige Anfrage begrenzt oder ausdrücklich zurückgesetzt werden.
Aussagekraft der veröffentlichten Zahlen
Die am 25. August 2026 ergänzten Zahlen nennen 75 bis 280 ms bei einem warmen, bereits im Speicher geladenen Prozess. Sie verweisen auch auf sieben Tage Produktionslogs: 133 Anfragen nach mehr als zwanzig Minuten ohne Traffic, alle in unter 615 ms beantwortet.
Die am 11. Juli 2026 beobachtete Produktionskonfiguration war eine Laravel-Cloud-Instanz vom Typ flex-1gb mit aktiviertem Ruhezustand. Nach einer Zeit ohne Traffic ist ein Aufwachen daher möglich. Das beweist aber nicht, dass die Instanz vor jeder dieser Anfragen im Ruhezustand war.
Ohne Aufzeichnungen und Messprotokoll lässt sich die angegebene Dauer schwer einordnen. Eine Messung auf Anwendungsebene kann das Aufwachen der Instanz, die Proxy-Verarbeitung und den Netzwerkweg zum Browser ausschließen. Diese Zahlen geben daher keine Auskunft darüber, wie lange ein Besucher wartet.
In seiner Ankündigung vom 1. Juni 2026 nennt Laravel Cloud eine Aufwachzeit von unter 500 ms für den gesamten Stack. Der Anbieter gibt eine Verkürzung um den Faktor zwanzig gegenüber der Vorgängergeneration an, mit ein bis vier vCPUs je nach Bedarf.
Die Zahlen von aubia.dev reichen nicht aus, um diese Anbieterangabe zu bestätigen. Ein Vergleich dieser Website mit PHP-FPM liegt nicht vor.
Das Server-Rendering von Inertia
Bei einer ausschließlich clientseitig gerenderten React-Seite muss JavaScript erst geladen und ausgeführt werden, bevor der Inhalt erscheint. Nicht alle Crawler führen es aus, und manche erledigen das erst später.
Mit Inertia v3 SSR sendet Laravel die Seite und ihre Daten an einen Node-Prozess. Node rendert die React-Komponenten und gibt HTML an Laravel zurück, das es über die Blade-View in die Antwort einfügt.
Seiteninhalt und Metadaten sind damit in der initialen HTML-Antwort verfügbar. Das Rendering braucht Zeit: Laravel muss auf das Ergebnis von Node warten, bevor es die Seite sendet.
Im Browser hydriert React das empfangene HTML und macht damit Buttons, Formular und weitere Komponenten interaktiv.
Das Diagramm zeichnet einen ersten Besuch nach, von der Browseranfrage bis zur Hydration.
Der Wartelistenzähler gehört nicht zu diesem ersten Rendering. Er kommt über eine spätere Anfrage, die weiter unten beschrieben ist.
In der Entwicklung übernimmt Inertias Vite-Plugin das SSR, ohne dass ein separater Rendering-Server gestartet werden muss. In Produktion bleibt der Node-Prozess vom PHP-Server getrennt und muss zusätzlich betrieben werden.
Die Konfiguration des SSR ist in config/inertia.php deklariert:
// config/inertia.php, Auszug
'ssr' => [
'enabled' => (bool) env('INERTIA_SSR_ENABLED', true),
'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
'url' => env('INERTIA_SSR_URL'), // interne Standardadresse ausgelassen
'ensure_bundle_exists' => (bool) env('INERTIA_SSR_ENSURE_BUNDLE_EXISTS', true),
'throw_on_error' => (bool) env('INERTIA_SSR_THROW_ON_ERROR', false),
],
Mit ensure_bundle_exists unterbleibt der Rendering-Versuch, wenn das SSR-Bundle fehlt: Inertia gibt null zurück, ohne ein Fehlerereignis auszulösen. Scheitert ein Rendering-Versuch, löst Inertia SsrRenderFailed aus. Auf dieser Website verarbeitet kein Listener der Anwendung dieses Ereignis.
Die Einstellung throw_on_error auf false erlaubt dann den Rückfall auf clientseitiges Rendering. Die View app.blade.php behält einen allgemeinen Titel und eine allgemeine Beschreibung. Der Seiteninhalt und seine spezifischen Metadaten hängen aber von JavaScript ab.
Bei einem vorübergehenden SSR-Ausfall ziehe ich diesen Fallback einer Fehlerseite vor. Das hat einen Preis: Inhalte können später erscheinen, und ein Crawler ohne JavaScript erhält nicht mehr den erwarteten Inhalt. Ohne Listener fehlt außerdem eine gezielte Überwachung dieser Fehler auf Anwendungsebene.
Code Splitting mit Rolldown
Auf der Build-Seite nutzt aubia.dev Vite 8. Der Bundler ist Rolldown, in Rust geschrieben und nativ in Vite integriert.
Code Splitting verteilt das kompilierte JavaScript auf mehrere Dateien, die getrennt geladen werden. Es ist in den Build-Einstellungen ausdrücklich konfiguriert:
// 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,
},
],
},
},
},
},
Fünf Gruppen sind deklariert: React und sein Scheduler, Motion-Animationen, FontAwesome-Icons, der Inertia-Client und Radix-Komponenten. Diese Aufteilung erleichtert die Wiederverwendung gecachter Dateien über Deployments hinweg, sofern die erzeugten Dateien unverändert bleiben. Sie garantiert nicht, dass die Abhängigkeitsdateien nach jedem Build identisch sind.
Der Produktions-Build fügt ein dediziertes SSR-Bundle hinzu, erzeugt durch vite build --ssr.
Der Zähler nach dem initialen Rendering
Der Wartelistenzähler ruft die Gesamtzahl von aubia.cloud ab und nutzt einen serverseitigen Cache. Ist der Cache leer, kann dieser Netzwerkaufruf die Antwort verzögern.
Die Middleware HandleInertiaRequests stellt den Zähler deshalb als verzögert geladene Prop bereit. Der Browser fordert ihren Wert nach dem initialen Rendering mit einer zusätzlichen Anfrage an:
// app/Http/Middleware/HandleInertiaRequests.php
'waitlist' => [
'count' => Inertia::defer(fn(): ?int => app(FetchWaitlistCountQuery::class)->run(), rescue: true),
],
Das initiale Rendering wartet nicht auf diesen Aufruf. Fällt der Cache selbst aus, lässt rescue: true die Prop im Payload weg, ohne die verzögerte Anfrage scheitern zu lassen. Die Komponente behält dann ihre neutrale Anzeige.
Die zusätzliche Anfrage des Zählers geht ab, sobald die Seite angezeigt ist.
Laravel antwortet in JSON, ohne über Node zu gehen: Die Gesamtzahl stammt aus dem serverseitigen Cache oder von aubia.cloud.
Die Entscheidung für FrankenPHP
Die Datei config/octane.php verwendet standardmäßig RoadRunner, wenn keine Umgebungsvariable gesetzt ist:
// config/octane.php
'server' => env('OCTANE_SERVER', 'roadrunner'),
RoadRunner ist ein weiterer Server, der mit Octane-Workern kompatibel ist. Auch Swoole steht zur Verfügung, benötigt aber eine kompilierte PHP-Erweiterung.
Ich habe FrankenPHP wegen seines Worker-Modus und der Integration von Caddy in dieselbe Binärdatei gewählt. Die Unterstützung für HTTP/3 und automatische Zertifikate gehörte ebenfalls zu den Gründen im Architekturentscheidungsprotokoll des Projekts.
Diese beiden Fähigkeiten werden auf aubia.dev nicht genutzt: Der vorgeschaltete Produktionsproxy terminiert TLS und HTTP/3. Die integrierte Binärdatei vereinfacht den PHP-Teil des Deployments, SSR benötigt aber weiterhin einen separaten Node-Prozess.
Ein Wechsel zu RoadRunner würde die Installation seiner Binärdatei, Anpassungen an Konfiguration und Start sowie einen Deployment-Test erfordern. Die Variable OCTANE_SERVER wählt den Treiber aus; sie ersetzt diese Arbeit nicht.
Octane vermeidet die erneute Initialisierung von Laravel bei jeder Anfrage, und SSR liefert React-Inhalte in der initialen HTML-Antwort. Die veröffentlichten Zahlen bleiben historische Anhaltspunkte: Ohne überprüfbare Aufzeichnungen und Messprotokoll belegen sie weder einen Vorteil gegenüber PHP-FPM noch eine vollständige Aufwachzeit.
Dieses Projekt wird hier erzählt, Artikel für Artikel. Was als Nächstes kommt, hängt davon ab, was Sie dazu sagen.
Auf die Warteliste