DOM Virtual: Qual problema ela resolve?
Marcio Frayze
Posted on October 27, 2022
An English version of this article is available here.
Lembro vividamente uma conversa que tive com um colega de trabalho em 2014. Ele estava entusiasmado com uma biblioteca JavaScript desenvolvida internamente pelo Facebook e que a empresa havia decidido tornar open-source. Estou falando, claro, do React.
Como muitos na época, eu estava cético. Misturar JavaScript com Html?! Que coisa horrível! E o JSX então? Credo! Deixe meus templates HTML em paz! É óbvio que CSS deve ficar separado do HTML e o HTML separado do JavaScript! 😤
Minha opinião não era uma exceção. A comunidade em geral não foi muito receptiva às ideias do React. Em uma época em que muita gente, inclusive eu, ainda acreditava que a vinculação de dados de duas vias (two-way data binding) era o futuro, as ideias propostas pelo time do React eram bastante disruptivas e as pessoas responsáveis pela divulgação do projeto tiveram dificuldade em transmitir as vantagens desta nova abordagem.
Com o passar dos anos minha visão mudou, assim como a opinião de muitas das pessoas desenvolvedoras frontend. E como você deve saber, hoje React detém uma fatia significativa do mercado e influenciou muitas outras bibliotecas e frameworks (não só no mundo web mas de qualquer forma de interface gráfica, incluindo aplicativos móveis e desktop).
Mas ainda nesta época, nas primeiras versões do React, uma coisa me chamou atenção: essa tal de DOM Virtual. Fiquei intrigado com aquela ideia. Qual problema ela tentava resolver? 🤔
Afinal, o que é uma DOM Virtual?
Explicar para uma pessoa desenvolvedora o conceito de DOM Virtual é relativamente fácil. Não é nada mais que um objeto JavaScript que, de alguma forma, representa uma possível situação da DOM (Document Object Model) do navegador. A ideia é que, ao invés de alterar os objetos da DOM diretamente, seja mantida uma representação dela em memória em um objeto JavaScript. Quando quiser fazer alguma alteração na tela, deve-se criar uma nova DOM Virtual, com uma nova representação da página. A biblioteca irá então comparar esses dois objetos (o que representava a versão antiga da tela e o que representa a nova tela que deseja exibir), descobrir quais alterações precisam ser aplicadas na DOM do navegador para que ela fique igual ao que temos na DOM Virtual e, por fim, aplicar estas mudanças na DOM do navegador para sincronizá-la com a nova DOM Virtual.
Ao perguntar para meu colega qual era a vantagem de usar uma DOM Virtual, ele respondeu sem hesitar: “fazer alterações na DOM é muito custoso, portanto, fazer as mudanças utilizando uma DOM Virtual é muito mais performático!”. 🚀
Entendo que atualizar (ou criar um novo) objeto JavaScript é muito mais fácil e rápido que alterar a DOM real do navegador. Mas para exibir ou alterar algo na tela, temos que alterar a DOM de qualquer forma! Ou seja, com a inclusão da DOM Virtual, para fazer qualquer alteração visual, agora precisaria atualizar as informações em dois lugares: na DOM Virtual e na DOM do navegador. Esta segunda parte é feita de forma automática pelo próprio React (ou qualquer outra bibliteca/framework que utilize o conceito de DOM Virtual), mas ainda precisa ser feita. Sem isso, a usuária não seria capaz de ver a nova representação.
Por isso, não entrava na minha cabeça como algo assim poderia ser mais performático! O navegador iria ter que fazer o que já fazia antes (atualizar a DOM) e ainda ter muito trabalho a mais: atualizar a DOM Virtual, fazer o diffing do que foi alterado (comparando com a versão anterior), criar os patchs para que consiga tornar as 2 representações iguais e, finalmente, atualizar a DOM. 😮💨
💡 E a resposta para minha dúvida é realmente simples: DOM Virtual não deixa sua aplicação mais performática!
Ao continuar essa discussão com outras pessoas, me diziam que seria mais rápido para grandes aplicações. Mas eu não conseguia entender como isso seria possível… Pelo menos não desta forma mágica que estavam tentando me vender a ideia. 🧚
Resolvi então fazer o que toda pessoa teimosa como eu costuma fazer: me recusei a usar DOM Virtual. Mas como sou também curioso, me permiti ficar com essa ideia ali no fundo da cabeça, hibernando. Mas enquanto não entendesse a verdadeira motivação para sua criação e qual problema ela resolveria, eu não iria utilizá-la.
Minha primeira Single-Page Application: Implementando uma SPA de forma imperativa
Demorei um pouco para entrar na onda das single-page applications. Enquanto apareciam as primeiras versões do Angular, React, Elm e tantas outras soluções para criação de SPAs, no trabalho eu ainda estava implementando sistemas usando o antiquado JSF e, em casa, no máximo experimentava Ruby on Rails ou desenvolvimento de Apps nativos para celulares.
Mas em um dia de 2014 decidi implementar uma nova página no meu tempo livre e sentia que seria um bom momento para entender melhor como implementar uma SPA. Ela não está mais no ar mas, resumidamente, era uma webapp onde você podia encontrar (e cadastrar) lugares para doar comida.
No backend optei por utilizar a RackStep, uma biblioteca Ruby que eu estava desenvolvendo na época, em conjunto com MongoDB. E o deploy era feito na Heroku. Para o frontend, resolvi implementar tudo em JavaScript puro! Eu nunca faria isso em uma aplicação no trabalho mas, como era algo bastante experimental, optei por fazer tudo sem nenhuma biblioteca ou framework para tentar descobrir na prática - através de muita dor, tentativa e erro - quais eram os tipos de problemas que as bibliotecas e frameworks JavaScript poderiam me ajudar em futuros projetos. A pergunta que queria responder era essencialmente essa: qual a quantidade mínima de dependências que preciso para desenvolver uma SPA? Inicialmente pensei que seria zero. E assim foi, até a produção. Até os scripts de build foram feitos utilizando apenas o bom e velho bash script. Para ser honesto, usei algumas dependências para minimificar (reduzir o tamanho) dos arquivos. Utilizei também a API do Google Maps. Nada mais.
Logo no começo ficou claro que precisaria de alguma forma para controlar o estado da aplicação. O que está acontecendo? A pessoa está em qual etapa? Ela está lendo a descrição? Cadastrando um novo endereço? Buscando endereços em sua região? …
Esses eram alguns dos possíveis estados fáceis de prever e criar em um fluxo. Uma pequena máquina de estados era suficiente. Mas como tratava-se de uma SPA que se comunicava com um servidor, cada requisição poderia resultar em algum tipo de erro. O que deveria acotentecer quando uma requisição retornasse um erro? E se exceder o tempo limite? E se o endereço digitado não for encontrado? E se a pessoa não permitir o acesso a sua localização no navegador? …
🚨 Pra cada pergunta que tentava responder, a máquina de estados crescia exponencialmente, assim como a quantidade de bugs! 🐛
Uma das primeiras tomadas de decisão arquitetural foi optar por um fluxo síncrono. Mesmo sabendo que a experiência talvez ficasse um pouco degradada, cada etapa precisava estar muito bem definida. Se estava esperando uma requisição do servidor, qualquer outra ação na tela era bloqueada e mostrava uma animação no estilo “Aguarde, carregando informações”. Isso ajudou bastante! Mas não resolveu tudo. Os bugs continuavam aparecendo 🐛 e o código se tornava cada vez mais chato de manter. Claramente aquela solução não iria escalar bem. 🙁
Um outro problema recorrente era como sincronizar o que aparecia na tela com os dados da aplicação. Por exemplo: quando uma pessoa digitava um endereço, esta informação ficava gravada direto nos campos da tela. Quando ela pressionava um botão, era necessário:
- Obter estas informações diretamente da DOM (fazendo uma query nos campos de input);
- Validar a entrada;
- Caso fosse uma falha, alterar a DOM para exibir uma mensagem de erro e parar o fluxo;
- Bloquear a tela (exibindo uma mensagem de “Aguarde, carregando”), realizar a chamada AJAX para o backend e aguardar seu retorno;
- Validar o retorno do backend;
- Em caso de falha, exibir mensagem de erro e parar fluxo;
- Finalmente exibir uma mensagem indicando que o endereço foi cadastrado com sucesso!
Além disso tudo, ainda precisava tratar todos os demais eventuais tipos de erros que um processo assíncrono pode gerar.
Ao longo deste processo, algo começou a me incomodar bastante:
🚨 Estava guardando todo o estado do meu modelo de dados na DOM, no mesmo lugar onde deveria exibir a representação destes dados. Ou seja, a camada de visão estava se misturando com a camada de modelo/negócio.
Qualquer alteração visual poderia acarretar em mudanças nas queries que usava para obter e atribuir os valores de negócio. Isolei o máximo que podia, mas aquilo me incomodava.
Tentei diferentes abordagens para tentar separar as camadas, mas era muito difícil manter a sincronia entre estes dados. Seguia vagamente as ideias da vinculação de dados de duas vias (two-way data binding), mas era muito comum a camada de negócio ficar dessincronizada da camada de visualização (DOM). No final optei por deixar todo o estado na DOM. Foi a forma mais fácil para evitar dores de cabeça, mas eu não estava feliz. Sabia que aquela solução mais uma vez não escalaria bem. 🙁
No fundo do pensamento, às vezes surgia aquela pergunta: será que uma DOM Virtual me ajudaria aqui? Mas a resposta para esta pergunta só viria alguns anos depois.
Primeiros contatos com desenvolvimento de softwares de forma declarativa
Por volta de 2017 comecei a me interessar por programação funcional. Me joguei direto na linguagem mais extrema que encontrei na época: Haskell. Depois de ler muito e fazer alguns cursos online, mal conseguia sair do famoso “olá mundo”. 😬 Tive bastante dificuldade. Conseguia começar a ver as potenciais vantagens daquela abordagem, mas na prática não conseguia fazer quase nada. Frustrado, procurei por alternativas mais simples. Não queria desistir, mas resolvi postergar esta investida em Haskell. Claramente havia dado um passo maior que a perna, especialmente por não ter ninguém por perto com conhecimento necessário para me guiar de forma adequada. Foi quando decidi aprender a linguagem de programação Elm. 🌳
Elm tem muitas semelhanças com Haskell. Ambas são linguagem funcionais, fortemente tipadas e puras. A grande diferença é que Elm é focada no desenvolvimento de webapps, enquanto Haskell é uma linguagem para uso geral. Além disso existem várias outras diferenças técnicas que fazem com que Haskell seja uma linguagem mais complexa.
Meu objetivo era usar Elm como uma ponte: aprenderia os conceitos básicos através dela e depois migraria para Haskell. Mas no meio do processo me apaixonei pela filosofia do Elm e acabei postergando esta ida ao Haskell até os dias hoje. E foi através desta linguagem que tive finalmente uma primeira visão mais clara das vantagens de se utilizar aquela ideia de DOM Virtual! 🤩
A Arquitetura Elm
Para desenvolver webapps em Elm fui obrigado a entender a Arquitetura Elm (The Elm Architecture), também conhecida como Model View Update (ou apenas MVU). Já descrevi em detalhes este modelo neste vídeo e uma das caracterísiticas fundamentais é que trata-se de um modelo declarativo. E esta é a palavrinha mágica que, depois que entendi o significado, fez com que a DOM Virtual fizesse todo sentido.
💡 A motivação principal da criação da DOM Virtual é possibilitar uma abordagem declarativa no desenvolvimento da camada de visão de uma webapp.
A Arquitetura MVU é dividida em 3 partes:
- Model — uma estrutura de dados onde é armazenado o estado (modelo) da sua aplicação.
- View — uma função que recebe como parâmetro um model e retorna uma estrutura de dados representando uma nova versão da DOM (em outras palavras, retorna uma DOM Virtual).
- Update — uma função que permite “alterar” (criar um novo) model através de mensagens.
Não entrarei em detalhes de todo o modelo MVU neste artigo. Vou focar nas duas primeiras letras: M e V (Model e View). São elas que vão resolver boa parte dos problemas que descrevi ter encontrado em minha tentativa anterior, quando tentei criar minha primeira single-page application usando uma abordagem imperativa. E são elas que escondem o segredo das vantagens de se utilizar uma DOM Virtual.
Se quiser entender o modelo MVU na prática, recomendo assistir o meu vídeo que citei mais acima.
A função view
A função view é muito simples. Como disse anteriormente, a sua responsabilidade é bastante específica: a partir de um modelo, ela deve ser capaz de representar uma camada de apresentação equivalente. Você pode encará-la como sendo apenas uma função de transformação pura: a entrada é uma estrutura de dados representando o estado atual da aplicação e a saída é uma representação de o que deveria ser representado na tela para aquela condição do estado da aplicação. Toda vez que o estado da aplicação for alterado, a função view será re-executada, para obter qual a nova representação da tela.
💡 A função view permite que seja possível gerar toda a representação da tela “do zero”, ignorando qualquer contexto anterior. Não existe nenhum outro estado sendo mantido em lugar algum. A representação da tela sempre depende única e exclusivamente do estado atual da aplicação.
É esta característica que faz com que esta abordagem seja chamada de declarativa. Não importa a sequência de passos que levaram a aplicação até seu estado atual. A única coisa que importa é seu estado atual.
E esta é a grande diferença em relação à abordagem imperativa. Em uma implementação imperativa, é necessário ficar atento ao sincronismo entre a representação do estado da nossa aplicação e a representação da tela em si. Quando uma é alterada, é necessário certificar que a outra parte também seja sincronizada. Qualquer pequeno erro neste processo pode gerar inconsistências, e depurar este tipo de cenário pode ser bastante trabalhoso. Com auxilio da função view, esta sincronia torna-se totalmente transparente! Já que, como havia dito, toda vez que o modelo for alterado, a função view será re-executada.
Mas talvez você esteja pensando: e o caminho contrário? Quando alterar a tela, como o modelo será atualizado? E a resposta é simples:
💡 Você nunca irá atualizar a tela (objetos da DOM) diretamente! Por isso este modelo é chamado de fluxo de dados de via única (one-way data flow).
E como o modelo é atualizado?
Na arquitetura MVU, algumas regras são impostas. Além de não ser permitida a alteração dos objetos da DOM diretamente, também fica proibida a alteração do modelo (estado da aplicação) de forma direta. A única maneira de alterar o modelo é dentro da função update.
Mas esta parte começa a fugir um pouco do escopo da definição de uma DOM Virtual e, por isso, vai ficar para um outro artigo. 😉
Não poderia fazer tudo isso direto na DOM?
Uma última pergunta importante que precisa ser respondida é:
🤔 Não seria possível re-gerar a DOM inteira toda vez que ocorre alguma alteração, descartando assim a necessidade de uma DOM virtual e da aplicação dos patchs?
Em teoria, é possível. Mas esta abordagem traz dois grandes problemas.
O primeiro está relacionado com performance. Re-criar a DOM do navegador inteira toda vez que alguma alteração for necessária é muito custoso e demorado. Talvez isso se torne possível no futuro, mas hoje esta abordagem teria uma performance muito ruim até mesmo em páginas extremamente simples.
O segundo grande problema é ainda pior: a experiência de quem acessa a página seria muito ruim! Isso porque, da forma como os navegadores funcionam hoje em dia, a re-criação de toda a árvore de objetos da DOM faria com que o estado da barra de rolagem da página fosse perdido (a página voltaria para o topo o tempo inteiro), os elementos da tela iriam piscar, entre outros pequenos comportamentos que seriam perceptíveis para pessoa que está acessando nossa webapp.
A DOM Virtual resolve estes problemas e, ao utilizar uma biblioteca ou framework, este trabalho todo fica totalmente transparente para a pessoa que está implementando a página.
Outras abordagens
Embora Virtual DOM seja uma forma bastante adequada de resolver os problemas descritos ao longo deste artigo, nem toda biblioteca e framework moderno utiliza este conceito. A Svelte, por exemplo, utiliza uma outra abordagem. Desta forma, fica claro que existem alternativas para se resolver estes problemas, cada uma com suas vantagens e desvantagens.
Conclusões
Embora o conceito de DOM Virtual seja aparentemente simples, ela traz consigo uma forte mudança na forma de desenvolver interfaces gráficas.
As primeiras vezes que encontrei este conceito foi durante o desenvolvimento de webapps com a biblioteca React e na linguagem Elm, mas esta arquitetura se espalhou para os mais diversos tipos de interfaces gráficas. Os mesmos mecanismos de fluxo de dados de via única (one-way data flow) estão presentes hoje, por exemplo, no desenvolvimento de Apps nativos para celulares.
Depois de aprender a The Elm Architecture (ou Arquitetura MVU), ficou muito mais fácil entender o Jetpack Compose do Android, a SwiftUI do Apple, o mecanismo de criação e atualização das telas no Flutter e também, claro, entender melhor o React Native.
Sobre performance, é possível entender de onde surgiu a ideia de que a DOM Virtual traria um melhor desempenho. Realmente esta técnica possibilita que, com pouco esforço, a quantidade de alterações na DOM seja minimizada. Mas prefiro dar o crédito não à DOM Virtual, mas sim para a abordagem declarativa. A DOM Virtual é apenas uma ferramenta para alcançar esta forma de implementar interfaces.
Links interessantes
- A linguagem Elm: https://elm-lang.org
- A biblioteca React: https://reactjs.org
- Elm Programming - Virtual DOM: https://elmprogramming.com/virtual-dom.html
- Mozilla - Introduction to DOM: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction
- Elm - The Elm Architecture: https://guide.elm-lang.org/architecture
- Svelte - Virtual DOM is pure overhead: https://svelte.dev/blog/virtual-dom-is-pure-overhead
- Vídeo sobre o Hyperapp: https://www.youtube.com/watch?v=bxDITzQziDY
Gostou deste texto? Conheça meus outros artigos, podcasts e vídeos acessando: https://segunda.tech.
Posted on October 27, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.