Dependências assíncronas condicionais

Recursos da Web: Top-level await

O await de nível superior permite que os módulos atuem como funções assíncronas, ou seja, eles podem pausar a execução do módulo para aguardar promessas. Isso é extremamente útil para carregar condicionalmente dependências assíncronas, como polyfills ou bibliotecas secundárias pesadas, apenas quando exigido pelo navegador.

Ao usar o await de nível superior, é possível encapsular a lógica de carregamento condicional em um único módulo, impedindo que os módulos de consumo downstream sejam executados até que a dependência esteja totalmente carregada e pronta.

Padrão de polyfill condicional

Embora o await de nível superior possa ser usado para carregar condicionalmente qualquer dependência assíncrona que seja um módulo, um bom uso do padrão de carregamento de dependência condicional é carregar condicionalmente polyfills para navegadores que não oferecem suporte a um recurso específico. Essa abordagem encapsula a detecção de recursos e a importação dinâmica em um único módulo de dependência.

No caso a seguir, o polyfill do atributo popover é carregado condicionalmente se não estiver presente em HTMLElement.prototype:

// conditionally-load-polyfill.js

// Check if the feature is missing before doing work.
// MANDATORY: Prefer checking HTMLElement.prototype over window or document
// when checking for a global DOM attribute or property like popover.
if (!('popover' in HTMLElement.prototype)) {
  // Use top-level await to pause the execution of any module that imports this file 
  // until the polyfill finishes downloading and executing.
  await import('/path/to/popover-polyfill.js');
}

// Export a marker if needed by your application
export const polyfillLoaded = true;
// main.js

// MANDATORY: Because conditionally-load-polyfill.js uses top-level await, 
// this import will block execution of main.js until the polyfill is ready.
import './conditionally-load-polyfill.js';

// Now it is safe to use the feature (e.g., showing a popover)
const myPopover = document.getElementById('my-popover');
if (myPopover) {
  myPopover.showPopover();
}

Sequenciamento de importações de módulos e como evitar o bug do Safari 27

OBRIGATÓRIO: estruture o gráfico do módulo para que o módulo await de nível superior seja importado de um único ponto de entrada e seja concluído antes que os módulos downstream sejam avaliados:

  • NÃO coloque importações estáticas de mesmo nível em um arquivo de entrada (por exemplo, import './conditionally-load-polyfill.js'; import './app.js';). De acordo com a especificação de avaliação do módulo ECMAScript, o await de nível superior só suspende o módulo pai que o importa, não as importações estáticas de mesmo nível. Portanto, o ./app.js será avaliado simultaneamente em todos os navegadores antes que o polyfill termine de carregar.
  • NÃO importe o módulo await de nível superior de vários módulos irmãos simultaneamente (por exemplo, ter a.js e b.js importando estaticamente ./conditionally-load-polyfill.js). Embora o Safari 27 tenha corrigido o carregador de módulos do WebKit para oferecer suporte a importações simultâneas de módulos await de nível superior por especificação, o Safari 15–26.6 falha quando vários módulos importam simultaneamente um módulo que contém um await de nível superior (consulte a compatibilidade do navegador MDN).

Em vez disso, escolha um dos dois padrões, dependendo da estrutura do módulo:

  1. Módulo de entrada única (mostrado em main.js acima): se o módulo de entrada executar a lógica de inicialização dependente diretamente, importe ./conditionally-load-polyfill.js de forma estática na parte de cima desse módulo e execute o código no corpo dele.
  2. Inicialização de módulos de aplicativo separados: se o módulo de entrada delegar para módulos de aplicativo downstream (como ./app.js), encadeie chamadas dinâmicas de import() com await de nível superior para que ./app.js seja buscado e avaliado somente depois que o módulo de polyfill for resolvido:
// main.js

// MANDATORY: Chain dynamic imports with top-level await when bootstrapping separate
// application modules so './app.js' waits for the polyfill to finish loading.
// DO NOT use sibling static imports (`import './conditionally-load-polyfill.js'; import './app.js';`),
// which evaluate concurrently and execute './app.js' before the polyfill is ready.
await import('./conditionally-load-polyfill.js');
await import('./app.js');

Estratégias de fallback

Embora o await básico de nível superior seja compatível com os principais navegadores desde 2021 (Chrome 89, Edge 89, Firefox 89 e Safari 15), ele só alcançou a conformidade total com as especificações no Safari 27 (consulte a compatibilidade do navegador MDN).

  • Safari 15–26.6: se você seguir o padrão de ponto de entrada único na seção anterior para que vários módulos irmãos não importem simultaneamente o módulo await de nível superior, poderá confiar com segurança no await de nível superior sem um substituto adicional.
  • Navegadores legados (anteriores a 2021): se o aplicativo precisar ser compatível com navegadores sem suporte a await de nível superior, evite o await de nível superior e envolva a orquestração condicional de import() em uma função async padrão ou em uma cadeia de promessas .then().