Devlog · Fase 0 · 2/7
Octane, FrankenPHP e Inertia SSR em aubia.dev
Octane, FrankenPHP e Inertia SSR servem aubia.dev. Como funcionam, os limites da renderização no servidor e as ressalvas sobre números de latência publicados.
- laravel
- octane
- frankenphp
- inertia
- vite

Em aubia.dev, o Laravel permanece carregado em memória entre as requisições, e o React renderiza o conteúdo das páginas no servidor. O navegador recebe esse HTML, e depois o JavaScript adiciona interatividade.
Escolhi o Laravel Octane com FrankenPHP para executar a aplicação e o SSR do Inertia para renderizar as páginas React. Quero que os mecanismos de busca possam ler o conteúdo sem precisar executar o JavaScript do site.
Os números de latência publicados em 25 de agosto de 2026 apresentam dois resultados: 75 a 280 ms com o processo quente e 133 requisições abaixo de 615 ms após um período sem tráfego. Sem registros públicos e um protocolo de medição que permitam verificá-los, eles não comprovam uma melhoria em relação ao PHP-FPM nem um tempo de retomada.
O modo worker do Octane
Com PHP-FPM, o Laravel é inicializado a cada requisição. Os processos PHP podem atender várias requisições antes de serem reciclados, conforme sua configuração, incluindo pm.max_requests. A inicialização da aplicação se repete sem exigir um novo processo a cada vez.
O diagrama mostra o que o PHP-FPM repete a cada requisição.
Cada requisição seguinte passa novamente por essa inicialização.
O Laravel Octane inicializa o Laravel uma vez por worker. Esse worker atende as requisições seguintes com a aplicação já carregada. Os workers são reciclados ou reiniciados ocasionalmente, durante um deploy, por exemplo.
Em aubia.dev, o Octane usa o FrankenPHP, um servidor de aplicações PHP escrito em Go que integra o servidor web Caddy.
A variável de ambiente abaixo seleciona esse driver em uma instalação já configurada:
# .env: o driver ativo em aubia.dev
OCTANE_SERVER=frankenphp
Com Octane, as requisições passam por um único bloco de inicialização.
O bloco não reaparece entre duas requisições: a aplicação carregada atende as seguintes.
Manter a aplicação em memória exige atenção ao que ela armazena ali. Dados específicos de um visitante, armazenados em um singleton ou em uma propriedade estática, podem ser reutilizados para o próximo.
O Octane redefine o estado do framework entre requisições. Ele não limpa automaticamente todas as variáveis globais e propriedades estáticas do código da aplicação. Os serviços que retêm dados específicos de uma requisição devem ter seu escopo limitado a ela ou ser redefinidos explicitamente.
Os limites dos números publicados
Os números adicionados a este artigo em 25 de agosto de 2026 indicam 75 a 280 ms com um processo quente, já carregado em memória. Eles também fazem referência a sete dias de logs de produção: 133 requisições recebidas após mais de vinte minutos sem tráfego, todas atendidas em menos de 615 ms.
A configuração de produção observada em 11 de julho de 2026 era uma instância flex-1gb do Laravel Cloud com hibernação ativada. Um período sem tráfego torna possível uma retomada, mas não prova que a instância estava hibernando em cada uma dessas requisições.
Sem os registros e seu protocolo de medição, a duração informada continua difícil de interpretar. Uma medição na aplicação pode excluir a retomada da instância, o processamento pelo proxy e o trajeto pela rede até o navegador. Portanto, esses números não permitem determinar quanto tempo um visitante espera.
Em seu anúncio de 1º de junho de 2026, o Laravel Cloud informa um tempo de retomada inferior a 500 ms para toda a stack. O provedor afirma uma redução de vinte vezes em relação à geração anterior, com uma a quatro vCPUs conforme a demanda.
Os números de aubia.dev não bastam para confirmar a afirmação do provedor. Não há comparação disponível deste site com PHP-FPM.
A renderização no servidor do Inertia
Uma página React renderizada apenas no cliente depende do carregamento e da execução do JavaScript para exibir seu conteúdo. Nem todos os robôs de indexação o executam, e os que executam podem adiar esse trabalho.
Com o SSR do Inertia v3, o Laravel envia a página e seus dados a um processo Node. O Node renderiza os componentes React e devolve HTML ao Laravel, que o inclui na resposta por meio da view Blade.
O conteúdo e os metadados da página ficam disponíveis na resposta HTML inicial. A renderização leva tempo: o Laravel precisa esperar o resultado do Node antes de enviar a página.
No navegador, o React hidrata o HTML recebido para tornar os botões, o formulário e os demais componentes interativos.
O diagrama percorre uma primeira visita, da requisição do navegador até a hidratação.
O contador da lista de espera não faz parte dessa primeira renderização. Ele chega por uma requisição posterior, descrita mais abaixo.
Em desenvolvimento, o plugin Vite do Inertia cuida do SSR sem exigir a inicialização de um servidor de renderização separado. Em produção, o processo Node permanece separado do servidor PHP e precisa ser operado junto com ele.
A configuração do SSR é declarada em config/inertia.php:
// config/inertia.php, trecho
'ssr' => [
'enabled' => (bool) env('INERTIA_SSR_ENABLED', true),
'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
'url' => env('INERTIA_SSR_URL'), // endereço interno padrão omitido
'ensure_bundle_exists' => (bool) env('INERTIA_SSR_ENSURE_BUNDLE_EXISTS', true),
'throw_on_error' => (bool) env('INERTIA_SSR_THROW_ON_ERROR', false),
],
Com ensure_bundle_exists, a ausência do bundle SSR interrompe a tentativa de renderização: o Inertia retorna null sem emitir um evento de falha. Se uma tentativa de renderização falha, o Inertia emite SsrRenderFailed. Nenhum listener da aplicação trata esse evento neste site.
Definir throw_on_error como false permite então recorrer à renderização no cliente. A view app.blade.php mantém um título e uma descrição genéricos, mas o conteúdo da página e seus metadados específicos dependem do JavaScript.
Prefiro essa alternativa a uma página de erro durante uma indisponibilidade transitória do SSR. Há um custo: o conteúdo pode aparecer mais tarde, e um robô que não executa JavaScript deixa de receber o conteúdo esperado. Sem um listener, essas falhas também ficam sem monitoramento dedicado na aplicação.
O code splitting com Rolldown
No build, aubia.dev usa o Vite 8. O bundler é o Rolldown, escrito em Rust e integrado nativamente ao Vite.
O code splitting divide o JavaScript compilado em vários arquivos carregados separadamente. Ele é configurado explicitamente nas opções 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,
},
],
},
},
},
},
Cinco grupos são declarados: o React e seu scheduler, as animações Motion, os ícones FontAwesome, o cliente Inertia e os componentes Radix. Essa separação facilita a reutilização de arquivos em cache entre deploys se os arquivos gerados permanecerem inalterados. Ela não garante que os arquivos das dependências sejam idênticos após cada build.
O build de produção adiciona um bundle SSR dedicado, produzido por vite build --ssr.
O contador após a renderização inicial
O contador da lista de espera busca seu total em aubia.cloud, com um cache no servidor. Se o cache estiver vazio, essa chamada de rede pode atrasar a resposta.
Por isso, o middleware HandleInertiaRequests compartilha o contador como uma prop diferida. O navegador solicita seu valor em uma requisição adicional após a renderização inicial:
// app/Http/Middleware/HandleInertiaRequests.php
'waitlist' => [
'count' => Inertia::defer(fn(): ?int => app(FetchWaitlistCountQuery::class)->run(), rescue: true),
],
A renderização inicial não espera essa chamada. Se o próprio cache falhar, rescue: true omite a prop do payload sem fazer a requisição diferida falhar. O componente mantém então sua exibição neutra.
A requisição adicional do contador parte depois que a página é exibida.
O Laravel responde em JSON, sem passar pelo Node: o total vem do cache do servidor ou de aubia.cloud.
A escolha do FrankenPHP
O arquivo config/octane.php usa RoadRunner como padrão quando nenhuma variável de ambiente é definida:
// config/octane.php
'server' => env('OCTANE_SERVER', 'roadrunner'),
O RoadRunner é outro servidor compatível com os workers do Octane. O Swoole também está disponível, mas exige uma extensão PHP compilada.
Escolhi o FrankenPHP pelo modo worker e pela integração do Caddy no mesmo binário. O suporte a HTTP/3 e a certificados automáticos também constava no registro de decisão de arquitetura do projeto.
Esses dois últimos recursos não são usados em aubia.dev: o proxy de produção termina TLS e HTTP/3 antes de encaminhar as requisições. O binário integrado simplifica a parte PHP do deploy, mas o SSR ainda exige seu processo Node separado.
Uma migração para RoadRunner exigiria instalar seu binário, adaptar a configuração e a inicialização e depois testar o deploy. A variável OCTANE_SERVER seleciona o driver; ela não substitui esse trabalho.
O Octane evita reinicializar o Laravel a cada requisição, e o SSR fornece o conteúdo React na resposta HTML inicial. Os números publicados continuam sendo referências históricas: sem registros verificáveis e um protocolo de medição, eles não demonstram uma melhoria em relação ao PHP-FPM nem o tempo completo de retomada.
Esta construção é contada aqui, artigo após artigo. O que vem a seguir depende do que você disser sobre ela.
Entrar na lista de espera