ID: 0x

|

DATE:

Executando PHP no Navegador: Melhorias Recentes

AUTHOR:

|

READ_TIME: ~5 MIN

PHP é uma linguagem bloqueante, que executa um processo por requisição. Navegadores são ambientes assíncronos e de thread única. Como adaptar uma arquitetura profundamente orientada a servidores para rodar em um navegador?

O WordPress Playground responde a essa pergunta diariamente. A arquitetura compila PHP para WebAssembly via Emscripten, executa-o dentro de Web Workers e roteia requisições HTTP através de um Service Worker. Cada camada resolve uma incompatibilidade fundamental entre o PHP e o ambiente do navegador — desde E/S e acesso ao sistema de arquivos até redes e gerenciamento de subprocessos.

Este post aborda pull requests recentes que resolvem pontos de atrito específicos nessa arquitetura. E/S assíncrona, casos extremos de rede, bloqueio de arquivos SQLite e streaming de respostas — cada PR aborda um problema concreto que surgiu à medida que o Playground evoluiu de demonstrações rápidas para lidar com cargas de trabalho reais do WordPress inteiramente no navegador.

O problema de E/S assíncrona: Asyncify vs JSPI

Chamadas PHP como fread(), sleep() e curl_exec() bloqueiam a execução. Em WebAssembly, bloquear a thread principal congela o navegador. Essas chamadas precisam se tornar assíncronas.

Asyncify é a solução original. O Emscripten transforma o binário WASM em tempo de compilação, injetando código que salva e restaura a pilha de chamadas em cada ponto de suspensão potencial. Isso funciona em todos os navegadores, mas aumenta o tamanho e a complexidade do binário.

JSPI (JavaScript Promise Integration) adota uma abordagem diferente. O próprio runtime do WebAssembly lida com pausas e retomadas — sem necessidade de transformação em tempo de compilação. O resultado: binários menores, execução mais rápida e um modelo mental mais simples. A desvantagem? O JSPI atualmente funciona apenas no Chrome (V8).

A partir de março de 2026, os binários do Asyncify e do JSPI são mantidos em paralelo. PR #3335 recompilou o binário do Asyncify com as mesmas otimizações aplicadas ao JSPI, mantendo ambos os caminhos viáveis ​​enquanto a equipe avalia a melhor estratégia a longo prazo.

Polyfilling do JSPI para navegadores que não sejam o Chrome

O JSPI oferece melhor desempenho, mas o Safari e o Firefox ainda não o suportam. Dois PRs em versão preliminar exploram diferentes estratégias de polyfill — cada uma com vantagens e desvantagens distintas.

Transporte XHR síncrono (PR #3344, versão preliminar)

Quando o worker PHP precisa executar uma operação assíncrona, ele faz uma requisição POST XHR síncrona para um endpoint /_jspi/*. O Service Worker intercepta a requisição, executa o trabalho assíncrono e responde de forma síncrona. A thread de trabalho bloqueia durante a chamada XHR e desbloqueia com o resultado.

Essa abordagem não requer cabeçalhos de isolamento de origem cruzada, funciona dentro de iframes incorporados e oferece uma inicialização a frio 29% mais rápida em comparação com builds do Asyncify. Como utiliza o binário JSPI diretamente — sem a transformação em tempo de compilação do Asyncify — o projeto poderá eventualmente distribuir um único binário com um fallback XHR síncrono para navegadores que não sejam o Chrome.

SharedArrayBuffer (PR #3338, Draft)

A alternativa utiliza SharedArrayBuffer e Atomics.wait(). O worker grava uma requisição na memória compartilhada, chama Atomics.wait() para bloquear, e a thread principal executa a operação assíncrona e desperta o worker com Atomics.notify(). Isso oferece menor latência por meio do compartilhamento direto de memória, mas requer cabeçalhos COOP/COEP — que bloqueiam a incorporação de iframes em sites de terceiros.

A decisão entre essas abordagens permanece em aberto. O PR #3344 otimiza a capacidade de implantação, enquanto o PR #3338 otimiza o desempenho bruto. A restrição mais importante para o futuro do Playground — incorporação versus velocidade — determinará qual caminho prevalecerá.

Rede em um ambiente isolado (sandbox)

O WordPress pressupõe acesso HTTP livre. O Playground traduz as operações de socket PHP em chamadas fetch compatíveis com navegadores por meio de tcp-over-fetch. Vários PRs mesclados corrigem incompatibilidades específicas nessa camada de tradução.

Requisições não bloqueantes (PR #3306, Mesclado)

A classe WP_Http do WordPress suporta 'blocking' => false para requisições do tipo “disparar e esquecer”. Os plugins usam isso para análises, webhooks e tarefas cron. No Playground, todas as requisições eram bloqueadas — o que significava que /wp-cron.php impedia o carregamento da próxima página.

A correção verifica o parâmetro blocking na camada de transporte e retorna o controle imediatamente. O próprio contrato da API do WordPress agora funciona corretamente no ambiente de teste.

Uploads de arquivos com CURL (PR #3341, Mesclado)

Os uploads de arquivos com CURL ficavam travados indefinidamente. A causa raiz: um deadlock no protocolo HTTP/1.1. Quando o PHP envia uma requisição POST maior que 1024 bytes, o cURL inclui um cabeçalho Expect: 100-continue e pausa, aguardando a resposta 100 Continue do servidor antes de enviar o corpo da requisição. Enquanto isso, o tcp-over-fetch aguardava o corpo da requisição antes de iniciar a busca. Ambos os lados ficavam aguardando um pelo outro.

A correção detecta o cabeçalho Expect: 100-continue e envia imediatamente a resposta 100 Continue, desbloqueando o PHP para que ele possa prosseguir com o corpo da requisição.

Bufferização para conexões não HTTPS (PR #3356, Integrado)

A API Fetch do Chrome não permite o envio de corpos de requisição em streaming por conexões HTTP/1.1 — apenas HTTP/2. Quando o proxy CORS do Playground tentava novamente uma requisição TLS por HTTP puro, o fluxo era interrompido.

A correção armazena em buffer todo o corpo da requisição antes de enviá-lo quando o destino não é HTTPS, evitando a restrição de streaming do Chrome.

Bloqueios de sistema de arquivos e SQLite com múltiplos workers

O Playground usa SQLite em vez de MySQL por meio da integração com o wp-sqlite-db. Quando vários workers PHP acessam o mesmo banco de dados SQLite simultaneamente, eles precisam de bloqueio de arquivos para evitar corrupção — algo que os navegadores não oferecem nativamente.

Bloqueio em duas camadas (PR #3150, Integrado)

A implementação anterior de bloqueio de arquivos utilizava cerca de 900 linhas de JavaScript Emscripten escritas manualmente, que assumiam a semântica de bloqueio consultivo do Unix. No Windows, que utiliza bloqueios obrigatórios (o sistema operacional os impõe — nenhum processo pode ler uma região bloqueada), isso corrompia silenciosamente os bancos de dados sob carga de múltiplos processos.

A substituição introduz um CompositeFileLockManager em TypeScript com duas camadas:

  1. Bloqueios em memória coordenam instâncias WASM dentro de um único processo Node.js usando primitivas assíncronas que nunca bloqueiam a thread principal.
  2. Bloqueios nativos do sistema operacional protegem contra corrupção entre threads e entre processos usando flock() no Unix e LockFileEx/UnlockFileEx no Windows.

Essa distinção é importante porque o Unix usa bloqueios consultivos — os processos devem cooperar voluntariamente — enquanto o Windows usa bloqueios obrigatórios que o sistema operacional impõe no nível do kernel. A implementação anterior assumia semântica consultiva em todos os lugares, e é por isso que os usuários do Windows experimentavam corrupção silenciosa do banco de dados sob carga.

Otimização de Workers (PR #3308, Merged)

Quantos workers PHP o Playground deve executar? Os navegadores limitam as conexões HTTP/1.1 simultâneas a seis por origem. Uma conexão atende ao próprio Service Worker, deixando cinco para requisições PHP. Adicionar um sexto worker não aumenta a taxa de transferência — apenas aumenta o consumo de memória.

Reduzir de 6+ workers para 5 diminuiu o uso de memória de 2,59 GB para 1,68 GB — uma redução de 35%. Desempenho não é apenas “tornar mais rápido”. É entender as limitações da plataforma em que você está executando.

Streaming do Service Worker

Respostas em streaming (PR #3361, Merged)

O Service Worker anteriormente armazenava em buffer toda a resposta do PHP antes de retorná-la ao navegador. Para páginas pequenas, isso funcionava bem. Para operações grandes — importações e exportações de sites, downloads de arquivos — o tempo limite de 25 segundos do Service Worker no navegador interrompia a requisição.

A correção retorna uma Response em streaming imediatamente quando os cabeçalhos chegam. Os fragmentos do corpo da requisição fluem por meio de uma MessagePort do worker PHP para o Service Worker à medida que são gerados. Sem armazenamento em buffer, sem tempo limite.

Isso possibilita fluxos de trabalho reais: exportações de sites WordPress que podem atingir vários gigabytes, instalações de plugins que transmitem o progresso e operações de arquivos que antes expiravam por tempo limite.

O que isso significa para o Playground

Essas melhorias resolvem incompatibilidades fundamentais entre o PHP e o navegador. Requisições não bloqueantes, correções de protocolo HTTP, bloqueio adequado de arquivos, gerenciamento eficiente de memória dos workers e respostas em fluxo contínuo — cada uma dessas soluções resolve um problema específico que diferenciava uma demonstração de uma ferramenta de produção.

A decisão sobre o polyfill do JSPI permanece em aberto, e o resultado moldará a forma como o Playground distribuirá seus binários WebAssembly daqui para frente. Enquanto isso, os PRs mesclados já fazem uma diferença mensurável: menor uso de memória, eliminação de timeouts e comportamento correto para APIs do WordPress das quais os plugins dependem diariamente.

O WordPress Playground continua a evoluir. Consulte a documentação do Playground para obter guias e o repositório principal para acompanhar os últimos desenvolvimentos.

Agradecemos aos colaboradores do Playground, cujo trabalho tornou essas melhorias possíveis.


ENCODING: UTF-8

|

CHMOD: 644

// RELATED_ENTRIES

NEXT_READS

> cat ./comments.log

LOADING_ENTRIES…


> write ./comments.log –append

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *