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 por meio do processo de padronização da proposta. Como geralmente acontece com novos recursos na web, sua ordem de implementação e cronogramas podem diferir significativamente entre diferentes mecanismos. Se você quiser usar esses novos recursos, precisará garantir que nenhum dos seus usuários seja deixado de fora. Neste artigo, você aprenderá uma abordagem para fazer isso.

Alguns novos recursos melhoram o tamanho do código com a adição de novas instruções para operações comuns. Alguns adicionam primitivos avançados de desempenho e outros melhoram a experiência e a integração do desenvolvedor 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 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 a base de código separadamente para cada um desses grupos. Por fim, no navegador, você precisa detectar os recursos compatíveis e carregar os pacotes JavaScript e Wasm correspondentes.

Escolher e agrupar atributos

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

Uma tabela mostrando a compatibilidade dos navegadores com os recursos escolhidos.
Confira essa 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: linhas de execução, SIMD e processamento de exceções são compatíveis.
  • Firefox: Thread e SIMD são compatíveis, o processamento de exceções não é.
  • Safari: linhas de execução são compatíveis, mas não chipD e processamento de exceções.
  • Outros navegadores: pressupõem apenas o suporte a WebAssembly de referência.

Este detalhamento se divide por suporte a recursos na versão mais recente de cada navegador. Os navegadores modernos são contínuos e têm atualizações automáticas. Portanto, na maioria dos casos, você só precisa se preocupar com a versão mais recente. No entanto, desde que você inclua o WebAssembly de referência como coorte de substituto, ainda será possível oferecer um aplicativo funcional mesmo para usuários com navegadores desatualizados.

Como compilar para diferentes conjuntos de atributos

O WebAssembly não tem uma maneira integrada de detectar recursos compatíveis no ambiente de execução. Portanto, todas as instruções no módulo precisam ser compatíveis com o destino. Por isso, você precisa compilar o código-fonte no Wasm separadamente para cada um desses diferentes conjuntos de recursos.

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

Vou usar o SIMD por meio da emulação SSE2, linhas de execução com suporte à biblioteca Pthreads e escolher entre o processamento de exceções Wasm e a implementação alternativa do JavaScript:

# 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 próprio código C++ pode usar #ifdef __EMSCRIPTEN_PTHREADS__ e #ifdef __SSE2__ para escolher condicionalmente entre implementações paralelas (linhas de execução e SIMD) das mesmas funções e implementações em série no tempo de 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.

Carregar o pacote correto

Depois de criar pacotes para todas as coortes de recursos, é necessário carregar os pacotes corretos no aplicativo JavaScript principal. Para fazer isso, primeiro detecte quais recursos são compatíveis com o navegador atual. Você pode fazer isso com 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. Contanto que seu aplicativo ainda funcione para todos os usuários, essa abordagem pode fornecer um equilíbrio razoável entre aprimoramento progressivo e desempenho em tempo de execução.

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