Detecção de recursos do WebAssembly

Aprenda a usar os recursos mais recentes do WebAssembly e, ao mesmo tempo, oferecer suporte aos usuários em todos os navegadores.

O WebAssembly 1.0 foi lançado há quatro anos, mas o desenvolvimento não parou por aí. Novos recursos são adicionados pelo processo de padronização de propostas. Como acontece com novos recursos na Web, a ordem de implementação e os cronogramas podem variar significativamente entre os mecanismos. Se você quiser usar esses novos recursos, é necessário garantir que nenhum dos seus usuários seja deixado de fora. Neste artigo, você vai conhecer uma abordagem para isso.

Alguns recursos novos melhoram o tamanho do código adicionando novas instruções para operações comuns, outros adicionam primitivas de desempenho poderosas e outros melhoram a experiência do desenvolvedor e a integração com o restante da Web.

Confira a lista completa das propostas e as etapas no repositório oficial (link em inglês) ou acompanhe o status da implementação nos mecanismos na página oficial do roteiro de recursos.

Para garantir que os usuários de todos os navegadores possam usar seu aplicativo, você precisa descobrir quais recursos quer usar. Em seguida, divida-os em grupos com base no suporte ao navegador. Em seguida, compile sua base de código separadamente para cada um desses grupos. Por fim, no navegador, você precisa detectar os recursos com suporte e carregar o pacote JavaScript e Wasm correspondente.

Como selecionar e agrupar atributos

Vamos percorrer essas etapas escolhendo um conjunto arbitrário de atributos como exemplo. Digamos que eu identifiquei que quero usar SIMD, threads e tratamento de exceções na minha biblioteca por motivos de tamanho e desempenho. O suporte a navegadores é o seguinte:

Uma tabela mostrando o suporte do navegador aos recursos escolhidos.
Confira a tabela de recursos em webassembly.org/roadmap.

É possível dividir os navegadores nas coortes a seguir para garantir que cada usuário tenha a experiência mais otimizada:

  • Navegadores baseados no Chrome: há suporte para threads, SIMD e tratamento de exceções.
  • Firefox: a linha de execução e o SIMD têm suporte, mas o tratamento de exceções não tem.
  • Safari: as linhas de execução têm suporte, mas o SIMD e o tratamento de exceções não têm.
  • Outros navegadores: assumem apenas o suporte básico ao WebAssembly.

Essa divisão é feita por suporte a recursos na versão mais recente de cada navegador. Os navegadores modernos são atualizados automaticamente e sempre estão em dia. Portanto, na maioria dos casos, você só precisa se preocupar com a versão mais recente. No entanto, se você incluir a WebAssembly de referência como uma coorte de fallback, ainda será possível fornecer um aplicativo funcional, mesmo para usuários com navegadores desatualizados.

Compilação para diferentes conjuntos de recursos

O WebAssembly não tem uma maneira integrada de detectar recursos com suporte no momento da execução. Portanto, todas as instruções no módulo precisam ter suporte no destino. Por isso, é necessário compilar o código-fonte no Wasm separadamente para cada um desses conjuntos de recursos.

Cada conjunto de ferramentas e sistema de build é diferente, e você vai precisar consultar a documentação do seu compilador para saber como ajustar esses recursos. Para simplificar, vou usar uma biblioteca C++ de arquivo único no exemplo a seguir e mostrar como fazer a compilação com o Emscripten.

Vou usar SIMD com a emulação SSE2, threads com suporte à biblioteca Pthreads e escolher entre o processamento de exceções do Wasm e a implementação JavaScript padrão:

# First bundle: threads + SIMD + Wasm exceptions
$ emcc main.cpp -o main.threads-simd-exceptions.mjs -pthread -msimd128 -msse2 -fwasm-exceptions
# Second bundle: threads + SIMD + JS exceptions fallback
$ emcc main.cpp -o main.threads-simd.mjs -pthread -msimd128 -msse2 -fexceptions
# Third bundle: threads + JS exception fallback
$ emcc main.cpp -o main.threads.mjs -pthread -fexceptions
# Fourth bundle: basic Wasm with JS exceptions fallback
$ emcc main.cpp -o main.basic.mjs -fexceptions

O código C++ pode usar #ifdef __EMSCRIPTEN_PTHREADS__ e #ifdef __SSE2__ para escolher condicionalmente entre implementações paralelas (threads e SIMD) das mesmas funções e as implementações seriais no momento da compilação. Ela ficaria assim:

void process_data(std::vector<int>& some_input) {
#ifdef __EMSCRIPTEN_PTHREADS__
#ifdef __SSE2__
  // …implementation using threads and SIMD for max speed
#else
  // …implementation using threads but not SIMD
#endif
#else
  // …fallback implementation for browsers without those features
#endif
}

O processamento de exceções não precisa de diretivas #ifdef porque pode ser usado da mesma forma no C++, independentemente da implementação escolhida pelas flags de compilação.

Como carregar o pacote correto

Depois de criar pacotes para todas as coortes de recursos, é necessário carregar o correto do aplicativo JavaScript principal. Para isso, primeiro detecte quais recursos são compatíveis com o navegador atual. Para isso, use a biblioteca wasm-feature-detect. Ao combinar com a importação dinâmica, você pode carregar o pacote mais otimizado em qualquer navegador:

import { simd, threads, exceptions } from 'https://unpkg.com/wasm-feature-detect?module';

let initModule;
if (await threads()) {
  if (await simd()) {
    if (await exceptions()) {
      initModule = import('./main.threads-simd-exceptions.mjs');
    } else {
      initModule = import('./main.threads-simd.mjs');
    }
  } else {
    initModule = import('./main.threads.mjs');
  }
} else {
  initModule = import('./main.basic.mjs');
}

const Module = await initModule();
// now you can use `Module` Emscripten object like you normally would

Palavras finais

Nesta postagem, mostrei como escolher, criar e alternar entre pacotes para diferentes conjuntos de atributos.

À medida que o número de recursos aumenta,as coortes podem se tornar impossíveis de manter. Para aliviar esse problema, você pode escolher coortes de recursos com base em dados reais de usuários, ignorar os navegadores menos populares e permitir que eles retornem a coortes um pouco menos ideais. Desde que o aplicativo ainda funcione para todos os usuários, essa abordagem pode oferecer um equilíbrio razoável entre o aprimoramento progressivo e a performance de execução.

No futuro, o WebAssembly poderá ter uma maneira integrada de detectar recursos com suporte e alternar entre diferentes implementações da mesma função no módulo. No entanto, esse mecanismo seria um recurso pós-MVP que você precisaria detectar e carregar condicionalmente usando a abordagem acima. Até lá, essa abordagem continua sendo a única maneira de criar e carregar código usando novos recursos do WebAssembly em todos os navegadores.