O que os frameworks da Web resolvem e como fazer sem eles (Parte 1)

Publicados: 2022-03-10
Resumo rápido ↬ Neste artigo, Noam Rosenthal se aprofunda em alguns recursos técnicos comuns entre os frameworks e explica como alguns dos diferentes frameworks os implementam e quanto custam.

Recentemente, fiquei muito interessado em comparar frameworks com JavaScript vanilla. Começou depois de alguma frustração que tive usando React em alguns de meus projetos freelance, e com meu recente e mais íntimo conhecimento de padrões web como editor de especificações.

Eu estava interessado em ver quais são as semelhanças e diferenças entre os frameworks , o que a plataforma web tem a oferecer como uma alternativa mais enxuta e se é suficiente. Meu objetivo não é criticar os frameworks, mas sim entender os custos e benefícios, determinar se existe uma alternativa e ver se podemos aprender com ela, mesmo que decidamos usar um framework.

Nesta primeira parte, me aprofundarei em alguns recursos técnicos comuns entre os frameworks e como alguns dos diferentes frameworks os implementam. Também analisarei o custo de usar esses frameworks.

As Estruturas

Eu escolhi quatro frameworks para analisar: React, sendo o dominante hoje, e três novos concorrentes que afirmam fazer as coisas de forma diferente do React.

  • Reagir
    “O React facilita a criação de UIs interativas. As visualizações declarativas tornam seu código mais previsível e mais fácil de depurar.”
  • SolidJS
    “O Solid segue a mesma filosofia do React… No entanto, tem uma implementação completamente diferente que dispensa o uso de um DOM virtual.”
  • Esbelto
    “Svelte é uma nova abordagem radical para construir interfaces de usuário… uma etapa de compilação que acontece quando você cria seu aplicativo. Em vez de usar técnicas como a diferenciação virtual do DOM, o Svelte escreve código que atualiza cirurgicamente o DOM quando o estado do seu aplicativo muda.”
  • Aceso
    “Com base nos padrões de Web Components, o Lit adiciona apenas… reatividade, modelos declarativos e um punhado de recursos inteligentes.”

Para resumir o que os frameworks dizem sobre seus diferenciais:

  • O React facilita a construção de UIs com visualizações declarativas.
  • O SolidJS segue a filosofia do React, mas usa uma técnica diferente.
  • Svelte usa uma abordagem de tempo de compilação para UIs.
  • Lit usa padrões existentes, com alguns recursos leves adicionados.

O que os frameworks resolvem

Os próprios frameworks mencionam as palavras declarativo, reatividade e DOM virtual. Vamos mergulhar no que isso significa.

Programação declarativa

A programação declarativa é um paradigma no qual a lógica é definida sem especificar o fluxo de controle. Descrevemos qual deve ser o resultado, em vez de quais etapas nos levariam até lá.

Nos primeiros dias das estruturas declarativas, por volta de 2010, as APIs DOM eram muito mais simples e detalhadas, e escrever aplicativos da Web com JavaScript imperativo exigia muito código clichê. Foi quando o conceito de “model-view-viewmodel” (MVVM) tornou-se predominante, com os então inovadores frameworks Knockout e AngularJS, fornecendo uma camada declarativa JavaScript que lidava com essa complexidade dentro da biblioteca.

MVVM não é um termo amplamente usado hoje, e é uma variação do termo mais antigo “vinculação de dados”.

Ligação de dados

A vinculação de dados é uma maneira declarativa de expressar como os dados são sincronizados entre um modelo e uma interface do usuário.

Todas as estruturas de interface do usuário populares fornecem alguma forma de vinculação de dados e seus tutoriais começam com um exemplo de vinculação de dados.

Aqui está a vinculação de dados em JSX (SolidJS e React):

 function HelloWorld() { const name = "Solid or React"; return ( <div>Hello {name}!</div> ) }

Ligação de dados em Lit:

 class HelloWorld extends LitElement { @property() name = 'lit'; render() { return html`<p>Hello ${this.name}!</p>`; } }

Ligação de dados no Svelte:

 <script> let name = 'world'; </script> <h1>Hello {name}!</h1>

Reatividade

A reatividade é uma forma declarativa de expressar a propagação da mudança.

Quando temos uma maneira de expressar a vinculação de dados de forma declarativa, precisamos de uma maneira eficiente para a estrutura propagar as alterações.

O mecanismo React compara o resultado da renderização com o resultado anterior e aplica a diferença ao próprio DOM. Essa maneira de lidar com a propagação de alterações é chamada de DOM virtual.

No SolidJS, isso é feito de forma mais explícita, com sua loja e elementos internos. Por exemplo, o elemento Show acompanharia o que mudou internamente, em vez do DOM virtual.

No Svelte, o código “reativo” é gerado. O Svelte sabe quais eventos podem causar uma alteração e gera um código direto que traça a linha entre o evento e a alteração do DOM.

No Lit, a reatividade é realizada usando propriedades de elemento, essencialmente contando com a reatividade integrada de elementos personalizados HTML.

Mais depois do salto! Continue lendo abaixo ↓

Lógica

Quando uma estrutura fornece uma interface declarativa para vinculação de dados, com sua implementação de reatividade, ela também precisa fornecer alguma maneira de expressar parte da lógica tradicionalmente escrita de forma imperativa. Os blocos de construção básicos da lógica são "se" e "para", e todas as principais estruturas fornecem alguma expressão desses blocos de construção.

Condicionais

Além de vincular dados básicos, como números e string, todo framework fornece uma primitiva “condicional”. No React, fica assim:

 const [hasError, setHasError] = useState(false); return hasError ? <label>Message</label> : null; … setHasError(true);

O SolidJS fornece um componente condicional embutido, Show :

 <Show when={state.error}> <label>Message</label> </Show>

Svelte fornece a diretiva #if :

 {#if state.error} <label>Message</label> {/if}

No Lit, você usaria uma operação ternária explícita na função de render :

 render() { return this.error ? html`<label>Message</label>`: null; }

Listas

A outra primitiva de estrutura comum é a manipulação de listas. As listas são uma parte fundamental das UIs — lista de contatos, notificações, etc. — e, para funcionar com eficiência, elas precisam ser reativas, não atualizando a lista inteira quando um item de dados é alterado.

No React, o tratamento de listas se parece com isso:

 contacts.map((contact, index) => <li key={index}> {contact.name} </li>)

O React usa o atributo de key especial para diferenciar os itens da lista e garante que a lista inteira não seja substituída a cada renderização.

No SolidJS, os elementos internos for e index são usados:

 <For each={state.contacts}> {contact => <DIV>{contact.name}</DIV> } </For>

Internamente, o SolidJS usa sua própria loja em conjunto com for e index para decidir quais elementos atualizar quando os itens forem alterados. É mais explícito que o React, permitindo-nos evitar a complexidade do DOM virtual.

O Svelte usa a diretiva each , que é transpilada com base em seus atualizadores:

 {#each contacts as contact} <div>{contact.name}</div> {/each}

Lit fornece uma função de repeat , que funciona de maneira semelhante ao mapeamento de lista baseado em key do React:

 repeat(contacts, contact => contact.id, (contact, index) => html`<div>${contact.name}</div>`

Modelo de Componente

Uma coisa que está fora do escopo deste artigo é o modelo de componente nas diferentes estruturas e como ele pode ser tratado usando elementos HTML personalizados.

Nota : Este é um assunto grande, e espero abordá-lo em um artigo futuro porque este ficaria muito longo. :)

O custo

As estruturas fornecem vinculação de dados declarativa, primitivas de fluxo de controle (condicionais e listas) e um mecanismo reativo para propagar alterações.

Eles também fornecem outras coisas importantes, como uma maneira de reutilizar componentes, mas isso é assunto para um artigo separado.

Os frameworks são úteis? sim. Eles nos dão todos esses recursos convenientes. Mas essa é a pergunta certa a se fazer? Usar uma estrutura tem um custo. Vamos ver quais são esses custos.

Tamanho do pacote

Ao olhar para o tamanho do pacote, gosto de olhar para o tamanho reduzido não Gzip'd. Esse é o tamanho mais relevante para o custo da CPU da execução do JavaScript.

  • O ReactDOM tem cerca de 120 KB.
  • SolidJS tem cerca de 18 KB.
  • Lit é cerca de 16 KB.
  • Svelte tem cerca de 2 KB, mas o tamanho do código gerado varia.

Parece que os frameworks de hoje estão fazendo um trabalho melhor do que o React em manter o tamanho do pacote pequeno. O DOM virtual requer muito JavaScript.

Construções

De alguma forma, nos acostumamos a “construir” nossos aplicativos da web. É impossível iniciar um projeto front-end sem configurar o Node.js e um empacotador como o Webpack, lidando com algumas mudanças recentes na configuração do pacote inicial do Babel-TypeScript e todo esse jazz.

Quanto mais expressivo e menor o tamanho do pacote do framework, maior a carga de ferramentas de construção e tempo de transpilação.

Svelte afirma que o DOM virtual é pura sobrecarga. Eu concordo, mas talvez “construir” (como com Svelte e SolidJS) e mecanismos de modelo personalizados do lado do cliente (como com Lit) também sejam pura sobrecarga, de um tipo diferente?

Depuração

Com a construção e transpilação vem um tipo diferente de custo.

O código que vemos quando usamos ou depuramos o aplicativo da web é totalmente diferente do que escrevemos. Agora contamos com ferramentas especiais de depuração de qualidade variável para fazer engenharia reversa do que acontece no site e conectá-lo a bugs em nosso próprio código.

No React, a pilha de chamadas nunca é “sua” – o React lida com o agendamento para você. Isso funciona muito bem quando não há bugs. Mas tente identificar a causa das re-renderizações de loop infinito e você terá um mundo de dor.

No Svelte, o tamanho do pacote da biblioteca em si é pequeno, mas você enviará e depurará um monte de código gerado enigmático que é a implementação de reatividade do Svelte, personalizado para as necessidades do seu aplicativo.

Com o Lit, trata-se menos de construir, mas para depurá-lo efetivamente, você precisa entender seu mecanismo de modelo. Esta pode ser a maior razão pela qual meu sentimento em relação aos frameworks é cético.

Quando você procura soluções declarativas personalizadas, acaba tendo uma depuração imperativa mais dolorosa. Os exemplos neste documento usam Typescript para especificação de API, mas o código em si não requer transpilação.

Atualizações

Neste documento, examinei quatro frameworks, mas há mais frameworks do que posso contar (AngularJS, Ember.js e Vue.js, para citar alguns). Você pode contar com o framework, seus desenvolvedores, seu mindshare e seu ecossistema para trabalhar para você à medida que evolui?

Uma coisa que é mais frustrante do que corrigir seus próprios bugs é ter que encontrar soluções alternativas para os bugs do framework. E uma coisa que é mais frustrante do que os bugs do framework são os bugs que ocorrem quando você atualiza um framework para uma nova versão sem modificar seu código.

É verdade que esse problema também existe em navegadores, mas quando ocorre, acontece com todos e, na maioria dos casos, uma correção ou uma solução alternativa publicada é iminente. Além disso, a maioria dos padrões neste documento são baseados em APIs de plataforma web maduras; nem sempre há necessidade de ir com a borda do sangramento.

Resumo

Mergulhamos um pouco mais fundo na compreensão dos principais problemas que os frameworks tentam resolver e como eles os resolvem, focando na vinculação de dados, reatividade, condicionais e listas. Também analisamos o custo.

Na Parte 2, veremos como esses problemas podem ser resolvidos sem usar nenhuma estrutura e o que podemos aprender com ela. Fique ligado!

Agradecimentos especiais às seguintes pessoas pelas revisões técnicas: Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal e Louis Lazaris.