React Native: do Bridge ao Fabric, como essa arquitetura funciona? — Parte 01

Rafael Augusto Pena
Syngenta Digital Insights
11 min readJun 30, 2023

Em algum momento você já se perguntou sobre como o framework que você trabalha no seu dia-a-dia funciona por dentro? Detalhes do processo que você nunca sequer imaginou? Vamos falar hoje do React Native

Sem sombra de dúvidas, ter proficiência em um framework se tornou algo comum na vida de muitos desenvolvedores que buscam se especializar em algum ecossistema. Porém quando falamos em especialização, precisamos ir além de somente sabermos usar seus componentes e funcionalidade.

Ter um conhecimento mais profundo sobre o que você trabalha e a consciência sobre como os processos se comportam dentro da stack que você está consumindo, trarão uma visão mais refinada que auxiliará em tomadas de decisões complexas da sua equipe.

React Native

Buscando a especialização no ecossistema React Native, um bom primeiro passo se consiste no entendimento arquitetural do framework por debaixo do capô. Hoje possuímos duas principais arquiteturas:

  • Bridge (Primeira versão da arquitetura do React Native)
  • Fabric (Nova arquitetura beta, disponível a partir da versão 0.68, com melhorias de desempenho e comunicação do framework)

Perdido em conceitos básicos do React Native? Comece por aqui :D

Vamos começar "do começo". A primeira arquitetura implementada no React Native:

Introdução a Bridge

A arquitetura Bridge se consiste na primeira arquitetura criada pelo time do Facebook (Meta) buscando sustentar o framework em seu principal objetivo, a comunicação entre diferentes sistemas operacionais utilizando apenas um code-base, e entregando uma usabilidade que se igualasse ao desenvolvimento nativo.

Vamos entender como a Bridge funciona:

Quando iniciamos nossa análise sobre a arquitetura Bridge, é comum que visualizemos este diagrama:

Bridge architecture
Imagem 01 — Bridge architecture

Porém, para uma visão mais aprofundada do contexto, o diagrama abaixo apresenta com mais detalhes a comunicação entre cada artefato dessa arquitetura que exploraremos a seguir:

Imagem 02 — Bridge Architecture

Composição da JSThread

Começaremos nossa análise no bundle da aplicação, buscando entender todo o fluxo, desde o início do desenvolvimento em JS (JavaScript) do nosso app até o momento em que esse código será processado e se comunicará com o sistema operacional:

Imagem 03 — Bridge Architecture (Bundle)

Após o desenvolvimento de todo o pacote de código em JS, precisamos gerar o bundle comprimindo nosso code-base em um Bundler. No caso do React Native, utilizaremos o Metro, Bundler oficial do Meta Open Source.

Imagem 04 — Metro Bundler logo

Posteriormente a geração do bundle, precisaremos de um local dentro do sistema operacional, que não entende JS, para interpretá-lo. Para isso, precisaremos instanciar uma máquina virtual sustentando um intérprete JavaScript.

Dentro do mercado, podemos encontrar diversas opções de intérpretes JS:

Imagem 05 — JS Interpreters

No framework do React Native, o JavaScriptCore é o intérprete oficial. Por fazer parte do WebKit que empodera o navegador Safari no ecossistema Apple, não se faz necessário a instalação dele no iOS para que o JS seja executado. Porém, no Android, é necessário que ele venha instalado em conjunto do code-base do framework no momento do setup inicial do projeto.

Script para setup inicial de um projeto React Native: "npx react-native init NomeDoProjeto"

Utilizaremos o JavaScriptCore como intérprete do bundle JS gerado pelo Metro, e sustentaremos o JSCore (JavaScriptCore) funcionando através de uma máquina virtual instanciada em uma das 3 principais threads da arquitetura Bridge, a JSThread.

Imagem 06 — JSThread

O esquema completo da JSThread se assemelha a:

Imagem 07 — Bridge Architecture (JSThread)

Tendo conhecimento de como interpretamos o JS em um Sistema Operacional que não possui capacidade nativa de seu entendimento, o próximo passo se consiste em como comunicaremos os resultados dos processos executados do JS às APIs nativas do sistema operacional (lado nativo da arquitetura). Neste momento, entramos nos domínios do módulo Bridge, o coração do React Native até a versão 0.68.

Composição da Bridge

Imagem 08 — Bridge Module

Em conceitos básicos, a Bridge se assemelha a um antigo e consolidado pattern backend chamado de Message Broker. De maneira simplificada, o Message Broker é um padrão que, através do envio de dados por mensagens, sistemas de tecnologias diferentes conseguem se comunicar.

Exemplo:

Comunicação de server JAVA com um server NODEJS através de payloads no formato JSON.

Imagem 09 — Message Broker pattern

Seguindo este mesmo princípio, a Bridge torna possível o envio de resultados dos processos do código JS rodados em Runtime para a thread de processos nativos (Main), e vice-versa, através de mensagens do tipo JSON.

Posteriormente ao recebimento da mensagem JSON na Main Thread, a camada nativa tomará de 0 a N decisões e enviará de volta, através da Bridge, o resultado de sua decisão a JS (JSThread) por meio de uma nova mensagem JSON.

Abaixo podemos ver logs de mensagens reais sendo veiculadas pela Bridge no formato JSON:

Imagem 10 — JSON Message from JSThread to Native Thread (Main)

Entendendo o escopo geral do que o módulo Bridge faz dentro do framework React Native e o porquê dele ser considerado o coração da tecnologia, podemos buscar nos aprofundar um pouco mais. Para isso, vamos navegar pelo caminho que a mensagem percorrerá desde a thread remetente à destinatária dentro da Bridge.

Fluxo de mensageria da Bridge

A primeira coisa que precisamos recapitular é que basicamente possuímos dois sistemas que entendem "idiomas" diferentes e buscam se comunicar através de mensagens em um "idioma" comum, o JSON.

Porém, para que todo esse arcabouço de soluções apresentadas acima funcione, precisaremos de uma tecnologia intermediária capaz de sustentar a instância do nosso módulo de ponte de envio de mensagens (Bridge), rodar em runtime seus respectivos processos, também sustentar os "maquinários" capazes de manter a máquina virtual do JS na JSThread, seus respectivos processos e as APIs nativas do Sistema Operacional com seus respectivos processos na Main Thread (Nativa). Para a execução de todas essas tarefas, utilizaremos como tecnologia o C++.

Nosso módulo Bridge foi escrito em C++ e o utilizaremos para veicular as mensagens entre a instância da VM de JavaScript na Thread responsável pelo JavaScript e os processos nativos do sistema operacional na Thread Main.

Iniciando um zoom-in neste fluxo de veiculação, nos deparamos com o seguinte esquema interno de dados da Bridge:

Imagem 11 — Data's Bridge internal flow (iOS and Android)

O JavaScriptCore framework funciona nativamente em ambientes C, o que possibilita os inputs e outputs das mensagens entre o JS e a Bridge. Já a veiculação das mensagens entre a Bridge e a Main Thread (nativa) dependerá das ferramentas que cada sistema operacional oferece.

No caso do Android, utilizaremos a JNI para manter uma comunicação nativa entre o C++ e o Java. Já no iOS, podemos reutilizar as interfaces do Obj-C já presentes no ecossistema Apple.

Todas as mensagens JSON serão colocadas e orquestradas em filas para serem processadas e reprocessadas pelas camadas (JS ou Nativa).

Sumarizando, teremos algo parecido com isso:

Imagem 12 — React Native Bridge architecture summary

Observe que na camada do JavaScript, sempre processaremos 1 (uma) mensagem por vez.

Consumo de informações na Main Thread

Tendo um entendimento prévio do funcionamento do módulo Bridge e como ele orquestrará o input e output das mensagens da Thread JavaScript, precisamos buscar entender como faremos essa orquestração na Main Thread.

Buscando observar o diagrama abaixo, podemos perceber que uma mensagem enviada do JavaScript poderá ser processada em dois lugares diferentes da camada nativa. A Main Thread ou a Shadow Thread:

Imagem 13— JSON message in Native layer from Bridge Architecture

No momento em que a Bridge envia a mensagem do JS para o lado nativo, precisamos decidir se ela já está pronta para ser processada na Main Thread ou precisará passar por mais algum módulo adaptativo de conteúdo intermediário.

Para tomarmos essa decisão, precisamos verificar e garantir que o conteúdo das mensagens enviadas sejam compreensíveis pela camada nativa. Assim como quando estudamos um novo idioma onde muita das vezes em que traduzimos um texto se faz necessário algumas adaptações por conta de diferentes estruturas de sintaxe da linguagem e até mesmo diferenças culturais como por exemplo uma tradução do mandarim para o inglês, no nosso caso onde buscamos traduzir uma informação originada do JS para ser consumida pelas APIs nativas do sistema operacional, precisaremos também verificar a necessidade da adaptação do conteúdo.

Em nosso caso de tradução, precisaremos adaptar todo o conteúdo da mensagem que contenha informações relacionadas a gerenciamento de layout que produzimos no JavaScript. Isso porquê no JS utilizamos um sistema de desenvolvimento baseado no "FlexBox" que não é reconhecido pela engine de gerenciamento de UI nativa dos sistemas operacionais.

Conhece o FlexBox no React Native? Acesse este link para saber mais

Com isso, se faz necessário a presença de um middleware capaz de converter essas informações de UI escritas em "FlexBox" para o padrão da linguagem nativa do SO e, posteriormente, rearranjar a árvore de componentes da forma correta, garantindo que gastaremos o mínimo de processamento possível para atualizarmos a tela com as mudanças necessárias. Para gerenciar todo esse processo, utilizamos uma espécie de thread separada que chamamos de Shadow.

Shadow Thread e Yoga

Dentro da Shadow Thread, utilizaremos uma engine de layout cross-platform chamada Yoga Kit, desenvolvido pelo time open source do Facebook (Meta), capaz de entender todo o conteúdo em "FlexBox" e realizar um Binding para linguagens nativas dos SOs de todo o conteúdo.

Imagem 14 — Yoga Kit logo

Posteriormente ao processamento do Yoga, iremos rearranjar o layout da maneira mais econômica possível e enviaremos todas as informações para a Main Thread, e então será possível atualizarmos a tela.

A Shadow Thread orquestrará a Shadow Tree, uma abstração da lógica da Virtual Dom do framework React para calcular todos os processos do JS de mudança de UI

Fluxo de mensagens

Entendendo como funciona o sistema de mensageria da Bridge de uma ponta a outra, o último passo se torna entender qual é a ordem/fluxo que os processos geradores de mensagem seguirão a cada etapa de runtime do app, e assim, finalizarmos todo o caminho de ida e de volta de um dado dentro dessa arquitetura.

Dividiremos em duas principais etapas de runtime:

  • Start Up da aplicação
  • Interação do usuário na aplicação

Start Up

A etapa de Start Up visa a compreensão dos processos que são necessários para iniciar de um App sustentado no framework do React Native.

Para entender melhor o fluxo de processos e mensagens, podemos analisar o fluxograma a seguir:

Imagem 15 — Message Flow (RN Start Up Process)

Ao clicarmos para abrir o App, os primeiros processos a serem disparados serão os da camada nativa. Iniciaremos a Main Thread e carregaremos todos os Native Modules vinculados a aplicação.

Não sabe o que é um Native Module? Cheque este link aqui

Após termos todos esses processos nativos prontos para o trabalho, o próximo passo é iniciar a instância do módulo Bridge, e a partir dessa ação iniciar a instância da JSThread.

A JSThread então disparará alguns processos que verificarão se todas as dependências estão prontas para serem utilizadas.

Após a etapa acima, o JS então enviará uma mensagem via Bridge para o lado nativo com todas as informações necessárias de layout para serem renderizadas na tela, iniciando-se então um ciclo de troca de mensagens até que o App seja fechado.

Interação do usuário na aplicação

A etapa de Interação do usuário na aplicação visa a compreensão dos processos que são necessários para computar e reagir a um evento originado por meios externos (ações do usuário) em um App sustentado no framework do React Native.

Para entender melhor o fluxo de processos e mensagens, podemos analisar o fluxograma a seguir:

Imagem 16 — Message flow (RN User Interaction)

Utilizando como exemplo um evento de clique dentro do App, o primeiro lado que captará a informação será o nativo.

A partir disso, ele buscará coletar todas as informações necessárias para descrever este evento e enviará todos os dados para o JS através de uma mensagem assíncrona.

O JS, ao receber a mensagem, tomará uma decisão baseada nas implementações do bundle e fornecerá essa decisão à camada nativa da aplicação através de uma mensagem de resposta que poderá desencadear, ou não, em um processo para atualização do layout.

Em resumo, teremos sempre o JS sendo o protagonista nas decisões de processos, baseando-se nas nossas implementações de código, e a partir disso delegando ações a serem executadas pelas APIs nativas do sistema operacional.

Vantagens e desvantagens

Sem sombra de dúvidas a arquitetura baseada no módulo Bridge nos possibilitou darmos início a uma grande revolução no desenvolvimento de aplicações cross-platform.

Mais do que apenas manter N aplicações de N sistemas operacionais com apenas um único code-base, ela buscou através da estratégia de delegação de tarefas, continuar entregando uma performance e experiência bem próxima ao do desenvolvimento nativo.

Porém, com o passar do tempo, fomos identificando lacunas nessa abordagem arquitetural que nos causam alguns problemas e nos trazem desvantagens.

Ter um middleware entre a tomada de decisão e a decisão em sí possui impactos como por exemplo uma sobrecarga nas filas de mensagens a serem processadas, que podem causar side-effects na aplicação.

Outros problemas como carregarmos os Native Modules ainda no load inicial da aplicação e não apenas quando formos usar, também nos traz problemas de performance no load inicial do App.

E pensando em todos esses problemas citados, a segunda versão arquitetural do React Native foi lançada, o Fabric.

Uma proposta com o objetivo de resolver todos os problemas indicados acima já se tornou realidade a partir da versão 0.68 do framework e possui um futuro muito promissor, fortalecendo cada vez mais o ecossistema cross-platform de desenvolvimento React Native.

E para aprendermos mais sobre a nova arquitetura Fabric, proposta e implementada nas versões a partir da 0.68, teremos uma parte 02 do artigo logo!

Desde já agradeço sua leitura, feedbacks são sempre bem vindos!! :D

--

--

Rafael Augusto Pena
Syngenta Digital Insights

Software Engineer | React Native | iOS Swift | @Syngenta Digital