Pontos Principais
-
Os principais frameworks de frontend, como o React, estão ficando mais complexos à medida que recursos são adicionados. A complexidade adicional é visível nas ferramentas, sintaxe e ecossistema que vêm junto com esses frameworks;
-
Parte dessa complexidade vem do fato dos grandes frameworks necessitarem manter um alto nível de compatibilidade e estabilidade com versões anteriores devido à grande quantidade de usuários. Como tal, possuem um incentivo para não revisitar as principais opções de projeto;
-
O Crank revisita uma parte importante da arquitetura dos frameworks semelhantes ao React, que determina que as funções de renderização sejam puras. Em vez disso, o Crank aproveita os geradores assíncronos para realizar a renderização assíncrona gratuitamente. Geradores assíncronos são um recurso de linguagem padrão do JavaScript e não carregam o custo de uma biblioteca que implementa essa funcionalidade;
- Trabalhar com os geradores preparados para a linguagem e a sintaxe async/await permite que os desenvolvedores lidem com tarefas assíncronas (como buscar dados remotos, suspender e retomar a renderização) de forma tão natural quanto a execução das tarefas síncronas. A curva de aprendizado para dominar a implementação de uma aplicação frontend que são estranhos à linguagem tende a dimunuir.
Brian Kim, autor da biblioteca open source Repeater.js, lançou recentemente o Crank, uma nova biblioteca JavaScript para a criação de aplicações web. A originalidade do Crank vem descrito declarativamente no comportamento de uma aplicação com co-rotinas, implementadas em JavaScript com geradores assíncronos.
Embora o Crank ainda esteja no beta e mais pesquisas sejam necessárias, a renderização assíncrona permite lidar com casos de uso semelhantes à funcionalidade Suspense oferecida pelo React.
Quando o gerador é executado, ele pode receber dados (props) a partir dos quais retorna um iterador que tem acesso ao estado privado mantido no contexto do gerador. O iterador calcula e produz uma visualização sempre que solicitado pela parte iterativa (as funções da biblioteca Crank), com a última passando os props atualizados na sua solicitação de iteração. Um iterador Crank assíncrono retorna uma promessa que cumpre com a visualização pronta para ser renderizada, fornecendo recursos de renderização assíncrona. Portanto, os componentes do Crank suportam naturalmente o estado e os efeitos locais, sem a necessidade de uma sintaxe dedicada. Além disso, o ciclo de vida dos componentes é o ciclo de vida do gerador, sendo este último iniciado quando o elemento DOM correspondente é montado e é interrompido quando o elemento é desmontado. Erros podem ser identificados com tratamento padrão try... catch
do JavaScript.
Outros frameworks alavancaram geradores de JavaScript para criar aplicações web. Concur UI que foi migrado do Haskell para JavaScript, PureScript e Python utilizam geradores assíncronos na composição dos componentes. Turbine se autodescreve como um framework web puramente funcional sem compromissos e aproveita os geradores para implementar um paradigma FRP.
O InfoQ entrevistou Brian Kim sobre a razão deste novo framework JavaScript e os benefícios que acredita ser derivados do aproveitamento dos geradores JavaScript.
InfoQ: Pode nos contar um pouco sobre quem você é?
Brian Kim: Sou engenheiro front-end independente. Usei o React durante quase toda a minha carreira de programação, sendo possível até ver algumas menções minhas no blog React de 2013.
Também sou o criador e mantenedor da biblioteca open source de iteradores assíncronos, Repeater.js, que se esforça para ser o construtor que está faltando na criação de iteradores assíncronos seguros. [...] Criei repetidores, uma classe de utilitário que se parece muito com o construtor Promise e permite que converta APIs baseadas em callback em iteradores assíncronos com mais facilidade.
InfoQ: Pode nos dizer rapidamente qual é o objetivo dos repetidores?
Os repetidores usam várias boas práticas de projeto de iterador assíncrono que aprendi ao longo dos anos, como execução lazily, filas limitadas, lida bem com back-pressure e propaga erros de maneira previsível. Em essência, é uma API que foi cuidadosamente projetada para colocar os desenvolvedores num caminho de sucesso para usar iteradores assíncronos, garantindo que os manipuladores de eventos sejam sempre limpos e gargalos e bloqueios sejam descobertos rapidamente.
InfoQ: Recentemente, lançou o Crank.js, que foi descrito como um novo framework para a criação de aplicações web. Qual a necessidade de um novo framework JavaScript?
Kim: Sei que isso soa como um novo framework JavaScript no meio de vários que são lançados todas as semanas, e a postagem do blog que escrevo apresentando o Crank começa com um pedido de desculpas pela criação dele. Criei-o porque estava frustrado com as APIs React mais recentes, como Hooks e Suspense, mas ainda queria usar JSX e o algoritmo de comparação de elementos que o React popularizou. Usei o React felizmente por mais da metade de uma década, por isso demorou muito para dizer "chega" e escrever meu próprio framework.
InfoQ: Em que momento ocorreu esse "chega"?
Kim: Acho que minhas frustrações começaram com os hooks. Estava animado com a equipe do React investindo em tornar a sintaxe da função para componentes mais útil, permitindo que houvessem estados, mas estava preocupado com "As regras dos Hooks", que pareciam fáceis de serem contornadas e injustas com outros frameworks na medida em que bastava chamar dibs em qualquer função cujo nome começa com
use
. Quando comecei a aprender na prática mais sobre os hooks, vi os novos bugs de contexto que não víamos no JavaScript desde a invenção dolet
e doconst
, e por isso, comecei a me perguntar se os hooks eram de fato a melhor abordagem.Mas o verdadeiro ponto de inflexão para mim foi o projeto Suspense. [...]
InfoQ: Pode explicar melhor?
Kim: Comecei a experimentar o Suspense neste ponto, porque pensei que iria permitir usar hooks de iterador assíncrono que havia escrito como se fossem síncronos. No entanto, descobri que provavelmente seria impossível usar o Suspense, porque ele tem a obrigatoriedade do uso de cache, e não estava claro como poderia armazenar em cache e reutilizar os iteradores assíncronos que acionaram os hooks.
A percepção que o Suspense e a busca de dados assíncronos no React era necessário cache foi um tanto chocante para mim, porque até aquele ponto simplesmente presumia que obteríamos algo como async/await nos componentes React. [...] Estava muito preocupado em ter que digitar e invalidar todas as chamadas assíncronas que fiz apenas para usar as promessas.
[Percebi que] tudo que o React estava fazendo nos componentes com os métodos
componentDidWhat
ou com os hooks, poderia ser encapsulado em uma única função de gerador assíncrono:async function *MyComponent(props) let state = componentWillMount(props); let ref = yield <MyElement />; state = componentDidMount(props, state, ref); try { for await (const nextProps of updates()) { if (shouldComponentUpdate(props, nextProps, state)) { state = componentWillUpdate(props, nextProps, state); ref = yield <MyElement />; state = componentDidUpdate(props, nextProps, state,ref); } props = nextProps; } } catch (err) { return componentDidCatch(err); } finally { componentWillUnmount(ref); } }
[...] Produzindo elementos JSX ao invés de retorná-los, nos dava a possibilidade de escrever o código antes e depois da renderização, semelhante a
componentWillUpdate
ecomponentDidUpdate
. O estado se torna uma variável local, novas props podem ser passadas por meio de um iterador assíncrono fornecido pelo framework sendo possível até usar operadores de fluxo de controle JavaScript como try/catch/finally para capturar erros de componentes filhos e escrever uma lógica de limpeza, tudo dentro do mesmo escopo.
InfoQ: Então decidiu usar geradores assíncronos como base para um novo framework?
Kim: [...] Enquanto a equipe React gastou um considerável talento de engenharia construindo um "UI runtime", [percebi que] poderia simplesmente delegar as partes mais difíceis como suspensão da pilha ou agendamento de execução do JavaScript, que fornece geradores, funções assíncronas e uma fila de micro tarefas que fazem exatamente aquilo que é preciso. Senti que tudo o que a equipe React estava fazendo parecia impressionante e fora do meu alcance, já que um programador JavaScript padrão estava disponível, e só tinha que descobrir como as peças do quebra-cabeça se encaixavam.
O Crank é o resultado de uma investigação de meses sobre essa ideia de que os componentes podem ser escritos não apenas com funções de sincronização, mas com funções assíncronas e geradores de funções, o que é uma pequena mudança na minha vida, onde antes estava trabalhando loucamente em uma ideia nova. Honestamente, adoraria voltar a escrever aplicações e não um framework, mas o repentino interesse da comunidade JavaScript fez do Crank um acidente feliz.
InfoQ: Foi mencionado que o Crank.js alavanca os componentes orientados a JSX e os geradores assíncronos. Os componentes JSX são comuns em estruturas que usam funções de renderização (estruturas semelhantes ao React ou ao Vue em alguma medida). Poucos geradores são aproveitados nesse cenário e menos ainda sendo geradores assíncronos. Como essas construções se relacionam com o desenvolvimento de uma aplicação web?
Kim: Definitivamente, não sou o primeiro a experimentar geradores e geradores assíncronos. Estou sempre procurando novas ideias no GitHub e vi muitas pessoas no universo frontend experimentando-os.
No entanto, talvez devido à associação inicial de geradores em JavaScript com async/await e com as promises, muitas dessas bibliotecas pareciam usar os geradores para criar promessas e retornar apenas os elementos JSX, como uma forma de especificar dependências assíncronas para componentes. Inclusive, percebi que poderíamos simplesmente produzir elementos JSX e também descobri uma semântica separada para os componentes assíncronos com base no algoritmo de comparação virtual do DOM.
No fim, acho que os elementos e geradores JSX são, na verdade, uma combinação perfeita: Produz elementos, o framework os renderiza e os nós renderizados são passados de volta para o gerador em um tipo de padrão de request-response. Acho que, no geral, muitos desenvolvedores, em especial os que vêm de programação funcional, tendem a ter dificuldades com iteradores e geradores porque são estruturas de dados com estado, e o estado dos geradores torna mais difícil o raciocínio. Mas, na verdade, acho que é um ótimo recurso dos geradores e, pelo menos em JavaScript, a melhor maneira de modelar um processo com estado é de fato com uma abstração com estado.
Modelando o ciclo de vida do componente como geradores, não apenas podemos capturar e modelar o estado do DOM dentro de uma única função, como fazemos isso de uma maneira muito transparente, porque há apenas uma execução do gerador por instância de componente, e o contexto é preservado entre as renderizações. O número de vezes que um componente gerador síncrono é retomado no Crank é igual ao número de vezes que um pai o atualiza, mais o número de vezes que o componente se atualiza. Esse tipo de capacidade de raciocinar sobre o número exato de vezes que um componente é executado é algo que os desenvolvedores desistiram de entender principalmente com React e, na prática, significa que com Crank podemos colocar side-effects diretamente no "método de renderização", uma vez que a estrutura não está constantemente renderizando os componentes quando não estamos esperando.
InfoQ: E qual o retorno que teve dos desenvolvedores?
Kim: Recebi uma mensagem curiosa: "Gostaria de ter algo assim no Rust" e a partir dela fiquei animado com a possibilidade desses desenvolvedores pegarem as ideias do Crank e implementá-las em outras linguagens, que podem ter abstrações ainda mais poderosas, como no Rust, por exemplo.
InfoQ: O que seria mais fácil de fazer com o Crank do que com outros frameworks? Poderia nos dar um exemplo?
Kim: Pelo fato de todos os estados serem variáveis locais, ficamos livres para misturar os conceitos do React como props, state e refs dentro dos componentes do gerador de uma forma que nenhuma outra estrutura pode. Por exemplo, no código abaixo represento um componente que compara props novas e antigas e renderiza algo diferente com base na correspondência:
function *Greeting({nome}) { yield <div>Olá {nome}</div>; for (const {nome: novoNome} of this) { if (nome !== novoNome) { yield ( <div>Até mais {nome} e olá {novoNome}</div> ); } else { yield <div>Olá novamente {novoNome}</div>; } nome= novoNome; } } renderer.render(<Greeting name="Alice" />, document.body); console.log(document.body.innerHTML); // "<div>Olá Alice</div>" renderer.render(<Greeting name="Alice" />, document.body); console.log(document.body.innerHTML); // "<div>Olá novamente Alice</div>" renderer.render(<Greeting name="Bob" />, document.body); console.log(document.body.innerHTML); // "<div>Até mais Alice e olá Bob</div>" renderer.render(<Greeting name="Bob" />, document.body); console.log(document.body.innerHTML); // "<div>Olá novamente Bob</div>"
Não precisamos de um ciclo de vida separado ou um hook para comparar as props novas das antigas, apenas referenciamos ambas no mesmo contexto. Resumindo, comparar props torna-se tão fácil quanto comparar elementos de um array.
Além do mais, o Crank desacopla a ideia de estado local da renderização, sendo possível que muitos padrões de renderização avançados, que simplesmente não são possíveis em outras estruturas, sejam possíveis. Por exemplo, podemos imaginar uma arquitetura em que os componentes filhos têm estado local, mas não são renderizados novamente, mas depois renderizados de uma só vez por um único componente pai que renderiza em um loop de requestAnimationFrame. Componentes que têm estado, mas não precisam renderizar novamente toda vez que são atualizados, são fáceis de serem desenvolvidos no Crank porque separamos o estado da re-renderização.
Como exemplo, podemos conferir esta demonstração rápida que fiz. Nela implemento cubos/esferas 3D que os desenvolvedores React e Svelte estavam discutindo no Twitter no ano passado. Estou animado com o desempenho do Crank, uma vez que atualizar um componente é apenas passar pelos geradores, e há muitas otimizações interessantes que podemos fazer no espaço do usuário quando o estado é apenas uma variável local não estando fortemente acoplado a um sistema reativo que força cada componente com estado a ser renderizado novamente, mesmo se um componente pai o tivesse renderizado de qualquer maneira. Embora a versão inicial do Crank tenha se concentrado mais na exatidão e no projeto da API do que no desempenho, atualmente estou tentando tornar o Crank o mais rápido possível e os resultados estão começando a parecer promissores, embora ainda não possa fazer afirmações concretas sobre o seu desempenho.
InfoQ: Por outro lado, o que pode ser mais fácil de fazer com outros frameworks do que com o Crank?
Kim: Critiquei o Concurrent Mode e o futuro do React, mas se a equipe do React conseguir, será incrível ver os componentes que podem ser programados para renderização automaticamente com base em quão congestionada a thread principal está. Tenho algumas ideias sobre como implementar esse tipo de coisa no Crank, mas ainda não tenho soluções concretas. O fato de poder esperar diretamente nos componentes significa que seremos capazes de implementar o agendamento diretamente no espaço do usuário de uma forma transparente e opcional, isso me deixa bem otimista.
Além disso, embora não seja um fã dos hooks React, acho que há algo a ser dito sobre como os autores das bibliotecas podem encapsular as APIs inteiras em um ou dois hooks. Uma coisa que provavelmente deveria ter esperado, mas não esperava, foram os primeiros usuários clamando por funcionalidades semelhantes a hooks para integrar as bibliotecas com o Crank. Não tenho certeza de como seria ainda, mas também tenho algumas ideias.
Sobre o Entrevistado
Brian Kim é um engenheiro frontend independente. Criador e mantenedor da biblioteca open source de iteradores assíncronos Repeater.js, que tem como objetivo autointitulado, a criação de iteradores assíncronos seguros sem construtores.