Safari 27 beta conserta top-level await pela raiz: o que muda nos módulos ES

await no topo de um módulo parece uma extensão natural do JavaScript moderno: importe configuração, aguarde um recurso e só então exponha o módulo pronto. No Safari, porém, combinações de importação dinâmica, dependências compartilhadas e ciclos podiam produzir promessas resolvidas cedo demais ou exports ainda não inicializados.

A WebKit corrigiu o problema no Safari 27 beta reescrevendo o carregador de módulos. Não foi um ajuste pontual. A implementação antiga nasceu antes de top-level await e se apoiava numa proposta de Loader da WHATWG que acabou abandonada. O modelo novo acompanha o algoritmo atual de avaliação assíncrona de módulos ECMAScript.

Carregar, instanciar e avaliar não são a mesma coisa

O navegador primeiro encontra o grafo de dependências. Depois conecta imports e exports, criando os bindings. Só então executa cada módulo na ordem determinada pelo grafo. Com top-level await, a avaliação de um módulo pode suspender no meio. Quem depende dele deve esperar, mas partes independentes do grafo podem continuar.

// config.js
export const config = await fetch('/config.json').then(r => r.json());

// app.js
import { config } from './config.js';
start(config);

A promessa retornada por import('./app.js') só pode resolver depois que a avaliação assíncrona relevante terminou. Se o carregador confunde módulo encontrado com módulo avaliado, o consumidor enxerga um namespace cuja exportação ainda está na temporal dead zone. Daí erros como “Cannot access before initialization” que aparecem dependendo da ordem e do cache.

Importar o mesmo módulo duas vezes expunha a rachadura

Módulos ES são avaliados uma vez por instância. Duas importações dinâmicas do mesmo endereço deveriam compartilhar o trabalho e observar a mesma conclusão. Segundo a WebKit, a implementação anterior podia resolver chamadas em momentos incorretos e quebrar a ordem de avaliação, especialmente quando o grafo misturava módulos síncronos e assíncronos.

O carregador novo representa explicitamente os estados necessários e segue a propagação de dependências assíncronas. Quando um módulo aguarda, seus ancestrais dependentes ficam suspensos. Um ramo irmão sem dependência desse resultado não precisa parar. É uma coordenação de grafo, não uma fila global de arquivos.

O que muda para quem desenvolve

Código correto que falhava apenas no Safari deve se alinhar aos demais navegadores quando o Safari 27 chegar estável. Isso é especialmente relevante para bundlers, runtimes no navegador, módulos de configuração e bibliotecas que usam import() para inicialização preguiçosa.

Não significa que todo uso de top-level await virou boa ideia. Colocar uma operação lenta num módulo central bloqueia todos os seus dependentes. Um fetch sem timeout pode atrasar a inicialização inteira. Ciclos que já eram difíceis de raciocinar ficam ainda mais sensíveis quando a avaliação suspende.

  • Use top-level await quando o módulo realmente não tem estado válido antes da conclusão.
  • Mantenha dependências assíncronas longe de módulos amplamente compartilhados quando for possível expor uma função explícita.
  • Teste falha, timeout e importações concorrentes, não apenas o caminho feliz.
  • Inclua Safari 27 beta ou Technology Preview na matriz antes de remover workarounds.

Remover o workaround também exige cuidado

Se o projeto serializou importações, duplicou estado ou evitou top-level await para contornar o Safari, documente o motivo antes de apagar. Usuários não atualizam navegador no mesmo dia. O beta prova que a correção existe; não prova que toda a base instalada já a recebeu.

A história técnica aqui é maior que uma correção de compatibilidade. Uma implementação criada para uma especificação antiga conseguiu sustentar anos de evolução, até que o modelo assíncrono tornou a diferença incontornável. Às vezes, consertar pela raiz é menos arriscado que acrescentar mais uma exceção ao carregador.

Fonte