Como funciona o JavaScript: uma comparação com o WebAssembly + por quê, em certos casos, é melhor usá-lo sobre JavaScript

Este é o post nº 6 da série dedicada a explorar JavaScript e seus componentes de construção. No processo de identificação e descrição dos elementos centrais, também compartilhamos algumas regras práticas que usamos ao criar o SessionStack , um aplicativo JavaScript leve que precisa ser robusto e altamente eficiente para ajudar os usuários a ver e reproduzir seus defeitos de aplicativos da Web em tempo real. .

Abaixo estão os posts dessa série publicados até o momento

Desta vez, vamos desmontar o WebAssembly para analisar como ele funciona e, mais importante, como ele se compara ao JavaScript em termos de desempenho: tempo de carregamento, velocidade de execução, coleta de lixo, uso de memória, acesso à API de plataforma, depuração, multithreading e portabilidade.

A forma como construímos aplicativos da web está à beira da revolução — isso ainda é o começo, mas a maneira como pensamos sobre os aplicativos da web vai mudar.

Primeiro, vamos ver o que o WebAssembly faz

O WebAssembly (aka wasm ) é um bytecode eficiente de baixo nível para a web.

O WASM permite usar idiomas diferentes de JavaScript (por exemplo, C, C ++, Rust ou outro), gravar seu programa nele e, em seguida, compilá-lo (antecipadamente) para o WebAssembly.

O resultado é um aplicativo da web muito rápido para carregar e executar.

Tempo de carregamento

Para carregar JavaScript, o navegador tem que carregar todos os arquivos `.js` que são textuais.

O WebAssembly é mais rápido de carregar dentro do navegador porque apenas os arquivos wasm já compilados precisam ser transportados pela Internet. E wasm é uma linguagem semelhante a assembly de baixo nível com um formato binário muito compacto.

Execução

Hoje Wasm é executado apenas 20% mais lento que a execução de código nativo . Isto é, por todos os meios, um resultado surpreendente. É um formato que é compilado em um ambiente de sandbox e é executado em várias restrições para garantir que não haja vulnerabilidades de segurança ou que sejam muito resistentes a elas. A lentidão é mínima em comparação com o código verdadeiramente nativo. Além disso, será ainda mais rápido no futuro .

Melhor ainda, é independente de navegador — todos os principais mecanismos adicionaram suporte ao WebAssembly e oferecem tempos de execução semelhantes agora.

Para entender como o WebAssembly é mais rápido em comparação com o JavaScript, leia primeiro nosso artigo sobre como a engine JavaScript funciona.

Vamos dar uma olhada no que acontece no V8 como uma visão geral rápida:

Abordagem V8: compilação preguiçosa

À esquerda, temos algumas fontes JavaScript, contendo funções JavaScript. Primeiro, ele precisa ser analisado para converter todas as strings em tokens e gerar uma Abstract Syntax Tree (AST). O AST é uma representação na memória da lógica do seu programa JavaScript. Uma vez que esta representação é gerada, o V8 vai direto para o código de máquina. Você basicamente anda na árvore, gera código de máquina e lá você tem sua função compilada. Não há nenhuma tentativa real de acelerá-lo.

Agora, vamos dar uma olhada no que o pipeline V8 faz no próximo estágio:

Design de Pipeline V8

Desta vez, temos o TurboFan , um dos compiladores otimizados do V8. Enquanto seu aplicativo JavaScript está em execução, muito código está sendo executado dentro do V8. O TurboFan monitora se algo está lento, se há gargalos e pontos de acesso para otimizá-los. Ele os empurra através desse backend, que é um JIT otimizado que cria um código muito mais rápido para aquelas funções que estão mascando a maior parte do seu CPU.

Ele resolve o problema, mas a pegadinha aqui é que o processo de analisar o código e decidir o que otimizar também consome CPU. Isso, por sua vez, significa maior consumo de bateria, especialmente em dispositivos móveis.

Bem, a wasm não precisa de tudo isso — ela fica conectada ao fluxo de trabalho assim:

Design de Pipeline V8 + WASM

O wasm já passou por otimização durante a fase de compilação. No topo, a análise também não é necessária. Você tem um binário otimizado que pode ser conectado diretamente ao backend, o que pode gerar código de máquina.Todas as otimizações foram feitas pelo compilador no front end.

Isso torna a execução do wasm muito mais eficiente, já que algumas das etapas do processo podem ser simplesmente ignoradas.

Modelo de memória

WebAssembly confiável e não confiável

A memória de um programa C++, por exemplo, compilada no WebAssembly, é um bloco contíguo de memória sem “buracos”. Um dos recursos do wasm que ajuda a aumentar a segurança é o conceito de a pilha de execução ser separada da memória linear. Em um programa C++, você tem um heap, aloca a partir da parte inferior do heap e aumenta a pilha a partir do topo do heap. É possível pegar um ponteiro e depois procurar na memória da pilha para poder jogar com variáveis ​​que você não deveria tocar.

Esta é uma armadilha que muitos malwares exploram.

O WebAssembly emprega um modelo completamente diferente. A pilha de execução é separada do próprio programa WebAssembly, portanto não há como modificar dentro dela e alterar coisas como variáveis. Além disso, as funções usam deslocamentos inteiros em vez de ponteiros. As funções apontam para uma tabela de funções indiretas. E então, aqueles números diretos calculados saltam na função dentro do módulo. Ele foi construído dessa forma para que você possa carregar vários módulos wasm lado a lado, compensar todos os índices e tudo funciona bem.

Para mais informações sobre o modelo de memória e gerenciamento em JavaScript, você pode conferir nosso post muito detalhado sobre o tópico .

Garbage collection (Coleta de lixo)

Você já sabe que o gerenciamento de memória do JavaScript é tratado com um Garbage Collector.

O caso do WebAssembly é um pouco diferente. Suporta linguagens que gerenciam a memória manualmente. Você pode enviar seu próprio GC com seus módulos wasm, mas é uma tarefa complicada.

Atualmente, o WebAssembly é projetado em torno dos casos de uso C++ e RUST. Como o wasm é muito baixo, faz sentido que as linguagens de programação que estão apenas um passo acima da linguagem assembly sejam fáceis de compilar. C pode usar malloc normal, C++ pode usar ponteiros inteligentes, Rust emprega um paradigma totalmente diferente (um tópico totalmente diferente). Essas linguagens não usam GCs, então elas não precisam de todo o material de tempo de execução complicado para rastrear a memória. O WebAssembly é um ajuste natural para eles.

Além disso, essas linguagens não são 100% projetadas para invocar coisas JavaScript complexas, como a alteração do DOM. Não faz sentido escrever um aplicativo HTML inteiro em C ++ porque o C ++ não foi desenvolvido para ele. Na maioria dos casos, quando os engenheiros escrevem C++ ou Rust, eles têm como alvo o WebGL ou bibliotecas altamente otimizadas (por exemplo, cálculos de matemática pesada).

No futuro, no entanto, o WebAssembly suportará idiomas que não vêm com um GC.

Acesso à API da plataforma

Dependendo do tempo de execução que executa JavaScript, o acesso a APIs específicas da plataforma está sendo exposto, o que pode ser alcançado diretamente por meio de seu aplicativo JavaScript. Por exemplo, se você estiver executando JavaScript no navegador, terá um conjunto de APIs da Web que o aplicativo da Web pode chamar para controlar a funcionalidade do navegador/dispositivo da Web e acessar itens como DOM , CSSOM , WebGL , IndexedDB , API de áudio da Web etc. .

Bem, os módulos do WebAssembly não têm acesso a nenhuma API de plataforma. Tudo é mediado pelo JavaScript. Se você quiser acessar algumas APIs específicas da plataforma dentro do seu módulo de WebAssembly, você deve chamá-las por meio de JavaScript.

Por exemplo, se você deseja console.log , você deve chamá-lo por meio de JavaScript, em vez de seu código C++. E há uma penalidade de custo para essas chamadas de JavaScript.

Este não será sempre o caso. A especificação fornecerá APIs da plataforma para o wasm no futuro, e você poderá enviar seus aplicativos sem JavaScript.

Source maps (Mapas de origem)

Quando você minifica seu código JavaScript, você precisa de uma maneira de depurá-lo corretamente. É aí que os mapas de origem vêm para o resgate. 
Basicamente, os Mapas de Origem são uma maneira de mapear um arquivo combinado/reduzido para um estado não-compilado. Quando você cria para produção, junto com a compactação e a combinação de seus arquivos JavaScript, você gera um mapa de origem que contém informações sobre os arquivos originais. Quando você consulta um determinado número de linha e coluna em seu JavaScript gerado, pode fazer uma pesquisa no mapa de origem que retorna o local original.

O WebAssembly atualmente não suporta mapas de origem porque não há especificação, mas acabará (provavelmente em breve).

Quando você define um ponto de interrupção em seu código C++, você verá o código C++ em vez de WebAssembly. Pelo menos esse é o objetivo.

Multithreading

JavaScript é executado em uma única thread. Existem maneiras de utilizar o Event Loop e alavancar a programação assíncrona, conforme descrito detalhadamente em nosso artigo sobre o tópico .

JavaScript também usa Web Workers, mas eles têm um caso de uso muito específico — basicamente, qualquer computação intensa da CPU que bloquearia a thread principal da interface do usuário poderia se beneficiar da transferência para um Web Worker. No entanto, os Web Workers não têm acesso ao DOM.

O WebAssembly atualmente não suporta multithreading. No entanto, esta é provavelmente a próxima coisa a vir. Wasm vai se aproximar de threads nativas (por exemplo, threads no estilo C ++). Ter threads “reais” criará muitas novas oportunidades no navegador. E, claro, isso abrirá as portas para mais possibilidades de abuso.

Portabilidade

Atualmente, o JavaScript pode ser executado em praticamente qualquer lugar, do navegador ao servidor e até mesmo em sistemas embarcados.

O WebAssembly foi projetado para ser seguro e portátil. Apenas como JavaScript. Ele será executado em todos os ambientes que suportam wasm (por exemplo, todos os navegadores).

O WebAssembly tem o mesmo objetivo de portabilidade que o Java tentou alcançar nos primeiros dias com os Applets.

Onde é melhor usar o WebAssembly sobre JavaScript?

Nas primeiras versões do WebAssembly, o foco principal é em cálculos pesados ​​ligados à CPU (lidando com matemática, por exemplo). O uso mais comum que vem à mente são os jogos — há toneladas de manipulação de pixels lá. Você pode escrever seu aplicativo em C++/Rust usando ligações OpenGL às quais está acostumado e compilá-lo para wasm. E ele será executado no navegador.

Dê uma olhada nisso (Execute-o no Firefox) — http://s3.amazonaws.com/mozilla-games/tmp/2017-02-21-SunTemple/SunTemple.html . Isso está executando o motor Unreal .

Outro caso em que poderia fazer sentido usar o WebAssembly (performance-wise) é implementar alguma biblioteca que está fazendo um trabalho muito intenso de CPU. Por exemplo, alguma manipulação de imagem.

Como mencionado anteriormente, o wasm pode reduzir bastante o consumo de bateria em dispositivos móveis (dependendo do mecanismo), uma vez que a maioria das etapas de processamento foram concluídas antecipadamente durante a compilação.

No futuro, você poderá consumir binários do WASM mesmo se não estiver realmente escrevendo código que compila para ele. Você pode encontrar projetos no NPM que estão começando a usar essa abordagem.

Para manipulação de DOM e uso pesado de API de plataforma, definitivamente faz sentido ficar com JavaScript, já que não adiciona mais sobrecarga e tem as APIs fornecidas nativamente.

Na SessionStack , estamos constantemente forçando os limites do desempenho do JavaScript para escrever código altamente otimizado e eficiente. Nossa solução precisa fornecer um desempenho extremamente rápido, pois não podemos impedir o desempenho dos aplicativos de nossos clientes. Depois de integrar o SessionStack no aplicativo ou site da Web de produção, ele começa a registrar tudo: todas as alterações do DOM, interações do usuário, exceções do JavaScript, rastreamentos de pilha, solicitações de rede com falha e dados de depuração. E tudo isso ocorre em seu ambiente de produção, sem afetar o UX e o desempenho do seu produto. Precisamos otimizar nosso código e torná-lo assíncrono o máximo possível.

E não apenas a biblioteca! Quando você reproduz uma sessão de usuário no SessionStack, nós temos que renderizar tudo o que aconteceu no navegador do seu usuário no momento em que o problema ocorreu, e nós temos que reconstruir todo o estado, permitindo que você pule para frente e para trás no cronograma da sessão. Para tornar isso possível, estamos empregando fortemente as oportunidades assíncronas que o JavaScript fornece devido à falta de uma alternativa melhor.

Com o WebAssembly, poderemos aplicar alguns dos processamentos e renderizações mais pesados ​​em uma linguagem mais adequada para o trabalho e deixar a coleta de dados e manipulação de DOM para JavaScript.

Se você quiser experimentar o SessionStack, você pode começar de graça .Existe um plano gratuito que fornece 1.000 sessões/mês.

Referências:

Este é um artigo traduzido com a autorização do autor. O artigo original pode ser lido em https://blog.sessionstack.com/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it-d80945172d79

Autor do post original — Alexander Zlatkov— Co-founder & CEO @SessionStack