Detecção de recursos do WebAssembly

Aprenda a usar os recursos mais recentes do WebAssembly e oferecer suporte a 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, a ordem e os cronogramas de implementação podem variar significativamente entre os mecanismos. Se você quiser usar esses novos recursos, precisará garantir que nenhum dos seus usuários seja deixado de fora. Neste artigo, você vai conhecer uma abordagem para fazer isso.

Alguns recursos novos melhoram o tamanho do código com a adição de novas instruções para operações comuns, alguns acrescentam primitivos de desempenho poderosos 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 delas no repositório oficial. Também é possível acompanhar o status da implementação delas nos mecanismos da 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 sua base de código separadamente para cada um desses grupos. Por fim, no lado do navegador, é preciso detectar os recursos compatíveis e carregar o pacote correspondente do JavaScript e do Wasm.

Escolher e agrupar elementos

Vamos seguir essas etapas escolhendo um conjunto de recursos arbitrários como exemplo. Digamos que identifiquei que quero usar SIMD, linhas de execução e processamento de exceções na minha biblioteca por motivos de tamanho e desempenho. A compatibilidade com navegadores é a seguinte:

Uma tabela mostrando a compatibilidade dos navegadores com os recursos escolhidos.
Confira esta tabela de recursos em webassembly.org/roadmap.

Você pode dividir os navegadores nas seguintes coortes para garantir que cada usuário tenha a experiência mais otimizada:

  • Navegadores baseados no Chrome: Threads, SIMD e processamento de exceções são todos compatíveis.
  • Firefox: Thread e SIMD são suportados, mas o tratamento de exceções não é.
  • Safari: Threads são suportados, SIMD e tratamento de exceções não.
  • Outros navegadores: considere apenas o suporte básico do WebAssembly.

Esse detalhamento é dividido por suporte a recursos na versão mais recente de cada navegador. Os navegadores mais recentes são contínuos e se atualizam automaticamente. Portanto, na maioria dos casos, você só precisa se preocupar com a versão mais recente. No entanto, se você incluir o WebAssembly de referência como uma coorte de substituição, ainda poderá fornecer um aplicativo em funcionamento mesmo para usuários com navegadores desatualizados.

Compilar para diferentes conjuntos de atributos

O WebAssembly não tem uma forma integrada de detectar recursos compatíveis no ambiente de execução, portanto, todas as instruções do módulo precisam ser compatíveis com o destino. Por isso, é preciso compilar o código-fonte no Wasm separadamente para cada um desses 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 abaixo e mostrar como compilá-la com o Emscripten.

Vou usar o SIMD via emulação SSE2, linhas de execução com suporte à biblioteca Pthreads e escolher entre Processamento de exceções Wasm e implementação de JavaScript substituto:

# 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 as implementações seriais no tempo de compilação. Seria 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 tratamento de exceções não precisa de diretivas #ifdef, porque elas podem ser usadas da mesma maneira no C++, independentemente da implementação subjacente escolhida pelas flags de compilação.

Como carregar o pacote correto

Depois de criar pacotes para todas as coortes de recurso, você precisa carregar o correto do 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. Combinando-o 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, mostramos como escolher, criar e alternar entre pacotes para diferentes conjuntos de recursos.

À medida que o número de recursos aumenta,o número de coortes de atributos pode se tornar insustentável. Para aliviar esse problema, você pode escolher coortes de recurso com base nos seus dados de usuários reais, 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 oferecer um equilíbrio razoável entre aprimoramento progressivo e desempenho em tempo de execução.

No futuro, o WebAssembly poderá ter uma forma integrada de detectar recursos compatíveis e alternar entre diferentes implementações da mesma função no módulo. No entanto, esse mecanismo seria, por si só, 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 os novos recursos do WebAssembly em todos os navegadores.