Svelte: A era dos frameworks-compiladores

See other articles

Se você trabalha com front-end há algum tempo já deve ter percebido algo muito comum: a necessidade de adicionar dependências no seu código sempre que vai usar alguma biblioteca.

Seja na época do jQuery (hoje longe dos holofotes), seja atualmente com bibliotecas como React e frameworks como Angular, é consenso que para tais tecnologias funcionarem, antes devemos nos lembrar de adicionar as dependências que fazem tudo rodar corretamente.

Antigamente, por exemplo, era muito comum adicionarmos uma tag <script> contendo o jQuery sempre antes de carregar qualquer outro código ou plugin na nossa página:

<!-- Tags como essa eram comuns de se encontrar em sites na época de ouro do jQuery -->
<script src="scripts/jquery.min.js"></script>

Com o advento dos bundlers e task-runners como Gulp e Grunt, e mais recentemente Webpack, Rollup e Parcel, se tornou comum compilar as dependências junto do resto do seu código, fazendo com que cenários como esse:

<script src="scripts/jquery.min.js"></script>
<script src="scripts/datepicker.jquery.js"></script>
<script src="scripts/autocomplete.jquery.js"></script>
<script src="scripts/form-validator.jquery.js"></script>
<script src="scripts/lightbox.jquery.js"></script>

Fossem convertidos para isso:

<script src="scripts/bundle.min.js"></script>

Essa “otimização”, que juntava os arquivos (e dependências) todas em um único arquivo, favorecia bastante o carregamento das páginas em uma época onde o HTTP/2 ainda não era uma realidade.

Mas é importante lembrar que usar bundlers para agrupar todo o seu código em um único arquivo não resolve o problema de ainda ter que baixar e rodar essas dependências. No caso do React, são aproximadamente 117kb de código necessário só para interpretar a sua aplicação. Ou seja, um projeto React sem nenhum componente já sai do forno pesando mais de 110kb, logo de cara. Com o Angular isso não é diferente. Abaixo temos uma tabela demonstrando o peso de ambas as tecnologias:

Tecnologia Peso (em kb)
React ~117kb
Angular ~208kb

Fonte React: react.js + react-dom.js (link)
Fonte Angular: projeto criado do zero usando o CLI do Angular e compilado para produção.

É importante ressaltar que embora o peso do tráfego possa ser parcialmente resolvido usando compactadores como gzip ou Brotli, continuamos com outro problema: Esse código todo terá que ser executado e interpretado pelo navegador antes mesmo da sua aplicação começar a rodar.

Em outras palavras: frameworks com dependências e runtimes pesadas deixam sua aplicação pesada não só na hora de baixar, mas também na hora de executar.

Questionar o tamanho dos frameworks utilizados hoje em dia pode parecer papo de quem quer fazer otimizações prematuras e desnecessárias, mas é importante ressaltar que o hardware dos seus usuários não é o mesmo que o seu. O mundo real geralmente envolve pessoas com computadores bem mais lentos que os de um desenvolvedor, e smartphones com hardware muito mais limitado que o seu Android “parrudo” de última geração ou iPhone que “roda tudo liso”. Isso para não falar nos casos onde sua aplicação tem que rodar em dispositivos com baixa memória e pouco processamento, como no caso de interfaces para IoT. No mundo real, a performance do seu front-end importa. E é aí que entram os frameworks compiladores.

Svelte para que te quero

Logo do projeto Svelte
https://svelte.dev/

No final de 2016, uma proeminente personalidade do mundo front-end lançava um projeto junto de um artigo intitulado:

Frameworks sem framework: como nós não pensamos nisso antes?

A proposta era simples, porém revolucionária: compilar seus componentes para JavaScript puro em tempo de desenvolvimento. Essa mudança de paradigma passava a permitir que código de produção fosse 100% livre de dependências do framework utilizado.

É como se você pudesse escrever sua aplicação toda em React, e depois jogar o React fora, porque o navegador já saberia como entender o código dos seus componentes JSX no ambiente de produção.

Embora o conceito de compilar código para JavaScript puro em tempo de desenvolvimento não seja algo recente (GWT permitia fazer isso com Java há alguns anos), o Svelte trouxe esse conceito para dentro do próprio JavaScript. Agora não eram outras linguagens que compilavam para JavaScript puro, mas sim seus próprios componentes front-end.

Como o Svelte foi criado do zero pensando em ser um compilador mais do que um framework, otimizações puderam ser feitas desde o core do projeto nos seus mínimos detalhes, levando a resultados surpreendentes. O tamanho do código final da aplicação se torna muito menor do que tecnologias convencionais. A performance também fica muito acima da média, tanto na hora de executar quanto na hora de interpretar o código.

Atualizando nossa tabela de peso:

Tecnologia Peso (em kb)
React ~117kb
Angular ~208kb
Svelte 0kb

Mas e a sintaxe?

O Svelte possui uma sintaxe “mamão com açúcar” na hora de criar componentes: HTML, JavaScript e CSS com algumas pitadas a mais. Fácil assim.

A ideia de “reaproveitar” a sintaxe das três linguas-francas da web (HTML, CSS e JavaScript) não é merito do Svelte, uma vez que frameworks como Vue.js e Angular já seguem essa linha fazem anos (ao contrário do React, que parece seguir o caminho oposto com o JSX e conceitos como CSS-in-JS).

Mas em uma coisa o Svelte se destacou dos projetos citados acima: simplificar ao extremo a escrita de componentes.

Eis um exemplo do código necessário para um Hello World:

<!-- meu-componente.svelte -->
<h1>Olá, mundo.</h1>

É recomendado utilizar a extensão .svelte em arquivos de componentes.

Sim, é só isso. Como você pode ver, o Svelte descarta necessidade de instanciar classes, importar módulos, e outras verborragias bastante comuns em frameworks mais famosos. HTML puro é aceito, e você pode ir adicionando CSS e lógica (JavaScript) no seu componente conforme a demanda, através de uma sintaxe que lembra muito a simplicidade e objetividade do Vue.js:

<!-- meu-componente.svelte -->
<h1>Olá, mundo.</h1>

<style>
  h1 {
    color: red;
  }
</style>

O CSS fica restrito ao escopo do componente. Ou seja: um seletor h1 no seu componente não vai afetar o estilo de outros componentes da sua aplicação, mesmo caso estejam aninhados.

Adicionar lógica ao seu componente é tão simples quanto escrever JavaScript puro:

<!-- meu-componente.svelte -->
<script>
  let contador = 0;

  function incrementar() {
    contador = contador + 1;
  }
</script>

<button on:click={incrementar}>{contador} click(s).</button>

No HTML, qualquer código dentro de chaves {} é interpretado como JavaScript. Isso significa que você pode optar por escrever uma função no bloco script, ou diretamente no seu evento (inline). O código abaixo faz a mesma coisa que o código acima:

<!-- meu-componente.svelte -->
<script>
  let contador = 0;
</script>

<button on:click={() => contador = contador + 1}>{contador} click(s).</button>

Variáveis instanciadas dentro da tag script do seu componente se tornam automaticamente reativas, de forma que sempre que elas forem atualizadas, a sua aplicação também será automaticamente. O Svelte tem a inteligência de atualizar apenas o(s) pedaço(s) da sua aplicação que de fato sofreram atualizações.

Essa inteligência é possível por conta da arquitetura modularizada do compilador do Svelte, que cria blocos de códigos autônomos para cada pedaço do seu componente. Dessa forma, cada bloco consegue atualizar, modificar, adicionar ou remover informações do DOM de forma independente, e sem a necessidade de uso de um Virtual DOM como no caso do React. No caso do Svelte, as transformações são realizadas diretamente no DOM. Essa forma de lidar com o código é possívelmente uma das coisas que contribuem para a boa performance e baixo uso de memória do Svelte.

Declarações Reativas

Você também pode criar declarações que modificam uma variável automaticamente, toda vez que ela sofre alterações. No Svelte 3 esse conceito é chamado de Declaração Reativa:

<!-- meu-componente.svelte -->
<script>
  let contador = 0;
  let dobro;

  $: dobro = contator * 2;

  function incrementar() {
    contador = contador + 1;
  }
</script>
<h1>Contador: {contador}, Dobro do contador: {dobro}</h1>
<button on:click={incrementar}>Incrementar Valor</button>

No código acima, toda vez que a variável contador mudar, a declaração dobro será automaticamente recalculada com o valor atualizado (contador * 2).

A sintaxe $: parece estranha? E é. Porém, é JavaScript 100% válido. O nome desse declarador no JavaScript é o pouco conhecido Label.

Hoje em dia o Label foi deixado de lado no JavaScript puro por ser considerado confuso e de difícil leitura em códigos grandes e complexos. Porém no Svelte ele ganha uma sobrevida e muito mais poder ao ser utilizado pontualmente através das declarações reativas.

Two-way Bindings e Formulários

Lidar com formulários pode ser maçante por conta do trabalho manual de captura de valor nos campos. O Svelte facilita esse processo usando bindings:

<!-- meu-componente.svelte -->
<script>
  let nome = '';
  let ativo = false;
</script>

<input type="text" bind:value={nome}>
<input type="checkbox" bind:checked={ativo}>

Olá, meu nome é: {nome}. Estou ativo? { ativo ? 'Sim.' : 'Não.' }

Toda vez que o valor do campo ou o estado do checkbox mudar, as variáveis nome e ativo também mudarão automaticamente.

Uma forma ainda mais elegante de escrever a parte que verifica se o usuário está ou não ativo seria usando uma declaração reativa:

<!-- meu-componente.svelte -->
<script>
  let nome = '';
  let ativo = false;
  let estouAtivo;

  $: estouAtivo = ativo ? 'Sim.' : 'Não.';
</script>

<input type="text" bind:value={nome}>
<input type="checkbox" bind:checked={ativo}>

Olá, meu nome é: {nome}. Estou ativo? {estouAtivo}

Os bindings em formulários são de via dupla, ou seja: toda vez que o valor do campo mudar, ou toda vez que a variável for modificada no código, a mudança será refletida no componente.

Componentes Aninhados

Nada de novo por aqui, para instanciar um componente dentro de outro é só realizar um import como no React, por exemplo:

<!-- meu-componente.svelte -->
<script>
  import DatePicker from './DatePicker.svelte';
</script>

Seu componente será renderizado aqui: <DatePicker />

Você pode passar elementos (ou outros componentes) no corpo de um componente utilizando a tag <slot>:

<!-- meu-componente.svelte -->
<script>
  import Topo from './Topo.svelte';
</script>

<Topo>
  <h1>Posso passar HTML customizado por aqui!</h1>
</Topo>
<!-- Topo.svelte -->
<header>
  <slot>
</header>

Propriedades e Estados

Propriedades e Estados funcionam de forma bem semelhante ao React:

Para passar propriedades a um componente:

<!-- meu-componente.svelte -->
<script>
  import Titulo from './Titulo.svelte';
</script>

<Titulo texto="Titulo aqui" />
<!-- Titulo.svelte -->
<script>
  export const texto = '';
</script>

<h1>{texto}</h1>

Ao declarar uma constante com export, você está dizendo ao Svelte que essa constante é uma propriedade.

Para dizer ao Svelte que uma variável é um estado, basta trocar const por let:

<!-- Titulo.svelte -->
<script>
  export let texto = '';
</script>

<h1>{texto}</h1>

Se você remover o export, a variável (ou constante) perde o tratamento especial e volta a ser apenas uma variável reativa dentro do seu componente.

Eventos Customizados

Algumas vezes você precisa passar informações de um componente para outro. Essas informações nem sempre são síncronas, e podem depender de uma ação do usuário, ou de uma requisição para uma API, por exemplo. Nesses casos, você pode utilizar eventos customizados (através do método createEventDispatcher) para esperar essas informações de um lado, e dispará-las de outro:

<!-- Botao.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();

  function disparaEvento() {
    dispatch('pow', {
      qualquer: 'coisa aqui'
    });
  }
</script>

<button on:click={disparaEvento}>Pow!</button>
<!-- meu-componente.svelte -->
<script>
  import Botao from './Botao.svelte';

  function fazAlgo() {
    alert('Disparou!');
  }
</script>

<Botao on:pow={fazAlgo} />

No exemplo acima, criamos um evento customizado chamado: pow. E ouvimos esse evento utilizando o atributo on:pow na instância do nosso componente.

Store, Transitions, Meta-componentes, Async-data, Ciclo de Vida, loops, …

Se eu continuasse a dar exemplos das features que o Svelte oferece, provavelmente acabaria reescrevendo toda a documentação do projeto, e o objetivo não é esse.

O objetivo aqui é simplesmente mostrar o poder de um compilador (ou seria framework?) front-end que compila para JavaScript puro, com um setup que pesa 0kb, é extremamente simples de utilizar, super-rápido e super-leve (ao ponto de ser escolhido como tecnologia usada nas maquininhas de pagamento Stone).

Porém mais do que um framework, o Svelte se mostrou uma tendência. E se todos os frameworks começassem a compilar para JavaScript puro? E se além disso eles também compilassem para componentes web nativos (Custom Elements)?

Essa tendência se espalhou e hoje não é mais representada apenas pelo pioneiro Svelte. Outros frameworks que fazem a mesma coisa já existem e estão disponíveis, como é o caso do Stencil, criado pela equipe do Ionic.

React e Angular também começaram a trabalhar em otimizações dos seus respectivos códigos. Novidades promissoras devem surgir com o amadurecimento do Angular Ivy, React Fiber e ferramentas como o Prepack. Cabe a nós acompanhar de perto, e sem preconceitos, as novidades que o futuro dos componentes front-end nos reserva.


Quero Saber Mais

Website: https://svelte.dev/

Aprenda Svelte com micro-aulas e escrevendo código: https://svelte.dev/tutorial

Playground: https://svelte.dev/repl

Para mais exemplos: https://svelte.dev/examples