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
- Uma visão geral do mecanismo, o tempo de execução e a pilha de chamadas
- Dentro do motor V8 do Google + 5 dicas sobre como escrever código otimizado
- Gerenciamento de memória + como lidar com 4 vazamentos comuns de memória
- O Event loop e o surgimento da programação assíncrona + 5 maneiras de codificar melhor com async/await
- Como o JavaScript funciona: Aprofundando em WebSockets e HTTP/2 com SSE + como escolher o caminho certo
- Como funciona o JavaScript: uma comparação com o WebAssembly + por quê, em certos casos, é melhor usá-lo sobre JavaScript
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:
À 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:
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:
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
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