Carregamento Preguiçoso & Divisão de Código: Por Que o React Native Não É Igual à Web (e Como Re.Pack Muda Isso)
Se você trabalhou tanto com React (web) quanto com React Native, provavelmente escreveu a mesma linha de código em ambos os lugares:
const DetalheDoProduto = React.lazy(() => import('./DetalheDoProduto'));E presumiu que faz a mesma coisa em ambos. Não faz. Este post explica o que realmente está acontecendo por trás das cortinas — no nível do bundler, não apenas no nível da API — por que essa diferença é suficiente para existir uma ecossistema de ferramentas (Re.Pack) para fechar a lacuna e onde as atualizações OTA se encaixam como alternativa mais simples para equipes que não precisam de Federação Completa de Módulos.
Parte 1: O Que a Divisão de Código Realmente Significa
Normalmente, um bundler percorre seu gráfico de importações a partir do arquivo de entrada e concatena tudo — cada tela, cada dependência, cada utilitário — em um único arquivo de saída. Divisão de código significa que o bundler produz vários arquivos de saída (“chunks”), e seu aplicativo carrega apenas um dado chunk quando realmente necessário.
O sinal que o bundler procura é uma importação dinâmica:
// importação estática — sempre embalada no arquivo principal
import DetalheDoProduto from './DetalheDoProduto';// importação dinâmica — candidato para ser dividido em seu próprio chunk
const modulo = await import('./DetalheDoProduto');React.lazy é apenas uma camada de proteção em torno disso:
const DetalheDoProduto = React.lazy(() => import('./DetalheDoProduto'));Parte 2: Como o Suspense Realmente Captura Isso
Esta é a parte que a maioria das explicações pula. Suspense não é "uma envoltória de spinner" — é construído em um mecanismo real: um componente pode lançar uma Promise ao invés de lançar um erro. Quando o componente do React.lazy é renderizado antes que sua importação tenha resolvido, ele lança a promessa pendente. O reconciler do React captura esse tipo específico de throw, sobe na árvore até a próxima fronteira Suspense, pausa aquela subárvore e renderiza o fallback em vez disso. Quando a promessa é resolvida, o React tenta renderizar novamente — desta vez o módulo está disponível.
import React, { Suspense } from 'react';const ProductDetail = React.lazy(() => import('./ProductDetail'));
const Reviews = React.lazy(() => import('./Reviews'));function ProductPage() {
return (
<div>
<Header /> {/* not wrapped — renders immediately */} <Suspense fallback={<ProductSkeleton />}>
<ProductDetail />
</Suspense> {/* separate boundary — Reviews loading doesn't block ProductDetail */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</div>
);
}
A otimização aqui é a granularidade da fronteira. Envolver toda a página em um Suspense faz com que a página inteira aguarde o componente lazy mais lento. Separar as fronteiras por seções independentes e as seções rápidas renderizam assim que estão prontas.
Este mecanismo de lançar uma promessa é genérico — também é como funciona a recuperação integrada ao Suspense (React Query’s useSuspenseQuery, Relay, Next.js server components), não apenas com React.lazy.
Parte 3: O que Realmente Acontece na Web (Vite / Webpack)
Em Vite ou Webpack, essa importação dinâmica import() é uma instrução real e estrutural. O bundler:
- Constrói
ProductDetail.jse seu gráfico de dependências em um arquivo de saída separado —chunk-a1b2c3.js. - Não inclui esse código em seu pacote principal.
- Em tempo de execução, quando a importação real do
React.lazyé executada, o navegador faz uma solicitação de rede real (GET /chunk-a1b2c3.js), analisa e avalia o código, então resolve a promessa.
É por isso que a divisão de código na web reduz o tamanho inicial do download — o navegador do usuário realmente nunca faz uma solicitação para ProductDetail.js até que ele navegue para algum lugar que precise dele.
main.js (240 KB) — enviado e analisado imediatamente
chunk-a1b2c3.js (40 KB) — carregado apenas quando ProductDetail renderiza
chunk-d4e5f6.js (25 KB) — carregado apenas quando Reviews renderizaParte 4: O que de fato acontece no React Native (Metro)
É aqui que o modelo mental da maioria dos engenheiros silenciosamente quebra. Metro — o bundler padrão do React Native — produz um único bundle JS, e todo esse bundle é incorporado no binário do aplicativo durante a compilação. Não há CDN, nem fetch HTTP em tempo de execução para uma parte ausente, porque quando o app está rodando, tudo já está no dispositivo.
Então, o que React.lazy(() => import('./ProductDetail')) realmente faz no Metro? Ele adiava a avaliação, não o download:
// Este código já foi instalado com o app.
// React.lazy apenas adia a *execução* dele até a primeira renderização.
const ProductDetail = React.lazy(() => import('./ProductDetail'));Web (Vite/Webpack):
- O código do módulo separado em seu próprio arquivo? Sim
- Carregado pela rede conforme necessário? Sim
- Execução adiada até ser necessária? Sim
- Reduz o que o usuário baixou? Sim
- Reduz o custo de execução inicial? Sim
React Native (Metro, padrão):
- Código do módulo separado em seu próprio arquivo? Não — um único pacote
- Fechado pela rede sob demanda? Não — já está no dispositivo
- Execução adiada até ser necessário? Sim
- Reduz o que o usuário baixou? Não
- Reduz o custo de inicialização? Sim
Esta é a verdadeira sutileza: a avaliação adiada ajuda de fato — reduz o tempo para interatividade e o pico da memória em grandes aplicativos, porque os efeitos colaterais do nível de módulo e o código de inicialização para telas que o usuário ainda não visitou simplesmente nunca são executados na inicialização. O que ela não faz é encolher o tamanho binário baixado pelo usuário da App/Play Store ou encolher a carga útil de uma atualização OTA — todo o pacote ainda é enviado como uma unidade.
Parte 5: Por Que Esta Brecha Existe
Historicamente, isso não era visto como um problema. Os aplicativos móveis baixam uma vez, instalam uma vez e funcionam offline-first — ao contrário de um site onde cada visitante re-faz o download dos recursos sobre uma conexão ativa em cada sessão. Havia menos pressão para construir verdadeira carregamento dinâmico de pacotes no design do Metro.
Mas duas coisas mudaram isso:
- O tamanho dos aplicativos cresceu — grandes aplicativos RN com muitos módulos de recurso pagam custos reais de inicialização e tamanho binário por enviar tudo de uma vez.
- As atualizações OTA se tornaram padrão — se apenas um módulo de recurso mudou, você ainda envia o todo pacote em cada push OTA, a menos que o pacote seja realmente dividido. Para um aplicativo médio, um pacote de 8 MB enviado para 500K usuários é de 4 TB de egresso — por uma única atualização.
Parte 6: Introdução ao Re.Pack — Divisão Real de Código para React Native
Re.Pack substitui os internos do Metro com Webpack ou Rspack, ambos dos quais têm suporte maduro e de primeira classe para Federação de Módulos — um padrão originalmente construído para micro-frontends da web, onde JS “remotos” independentemente construíveis podem ser carregados em tempo real por um aplicativo
// re.pack.config.mjs (conceitual)
import { ModuleFederationPlugin } from '@callstack/repack/plugins';export default {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
checkoutModule: 'checkoutModule@https://cdn.example.com/checkout/mobile.container.bundle',
},
shared: {
react: { singleton: true, eager: true },
'react-native': { singleton: true, eager: true },
},
}),
],
};// Consumindo um módulo remoto em tempo de execução — realmente obtido pela rede,
// não pré-baixado no binário do aplicativo anfitrião
const CheckoutFlow = React.lazy(() => import('checkoutModule/CheckoutFlow'));function App() {
return (
<Suspense fallback={<CheckoutSkeleton />}>
<CheckoutFlow />
</Suspense>
);
}Agora React.lazy + Suspense se comportam da mesma forma que no web: CheckoutFlow não está no binário do anfitrião em absoluto. Ele é obtido de uma URL CDN em tempo de execução, analisado e renderizado — e porque é saída compatível com bytecode Hermes construída pela ferramenta Re.Pack, ela roda nativamente como qualquer outro módulo RN.
O que isso realmente desbloqueia
- Módulos de recursos independentemente desejáveis — uma equipe de checkout pode enviar uma atualização para
checkoutModulesem disparar um lançamento completo da loja de aplicativos ou forçar todos os usuários a redescrever todo o aplicativo. - Menores payloads OTA — se apenas o módulo de checkout mudou, você só precisa enviar esse bloco, não todo o pacote, atacando diretamente o problema de largura de banda/egresso.
- Arquitetura verdadeira de micro-front-end em mobile — várias equipes podem possuir e implantar módulos remotos separados sob um único shell anfitrião.
Parte 7: A Honest Trade-off
Nada disso é gratuito, e uma resposta realista de engenharia tem que dizer isso:
- Você está substituindo o Metro — o bundler padrão, mais testado e com maior suporte da comunidade do RN — por uma configuração menos mainstream. As ferramentas de depuração e as suposições sobre compatibilidade embutidas nos fluxos de trabalho do RN CLI/Expo são menos bem-testadas com Re.Pack.
- A Federação de Módulos adiciona uma complexidade real na arquitetura — gerenciamento de versões de dependências compartilhadas entre o host e os remotos, contratos de versão/compatibilidade entre módulos implantados independentemente, e um modelo mental genuinamente mais difícil para equipes pequenas.
- A maioria dos aplicativos de uma única equipe não precisa disso. A complexidade justifica-se em escala verdadeiramente multi-equipe e grande aplicativo — equipes separadas enviando recursos independentes — não como uma atualização padrão para um aplicativo típico.
Parte 8: Atualizações OTA — A alternativa mais simples para o mesmo problema subjacente
Dê um passo atrás. O verdadeiro problema que a Federação de Módulos resolve para equipes móveis é: “apenas enviar o que mudou, não todo o aplicativo.” Re.Pack chega lá através da arquitetura — fragmentos remotos realmente carregados em tempo de execução. Mas se seu objetivo for especificamente a largura de banda e velocidade de lançamento, as atualizações Over-the-Air (OTA) resolvem uma versão mais estreita do mesmo problema com muito menos complexidade.
Como as OTA funcionam, mecanicamente: você cria um novo bundle JS, o envia para uma CDN e o aplicativo verifica e baixa-o ao iniciar — sem necessidade de revisão da App Store ou Play Store, pois você está alterando apenas JS e ativos acessíveis por JS, nunca código nativo compilado.
// Aplicativos Expo — EAS Update lida com isso nativamente
import * as Updates from 'expo-updates';async function checkForUpdate() {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync(); // aplica o novo bundle
}
}Onde o mercado realmente está em 2026 — vale a pena saber, pois isso mudou recentemente: CodePush, a primeira ferramenta OTA para React Native, é efetivamente morta como serviço hospedado — a Microsoft descontinou o CodePush do App Center em março de 2025. O código-fonte foi disponibilizado publicamente, então ainda é possível hospedar por conta própria, mas a maioria das equipes migrou para plataformas mais novas:
- EAS Update — Serviço de atualização gerenciada da Expo. Se você está usando o Expo, esta é a escolha padrão; inclui diferenciação de bytecode Hermes e lançamentos faseados integrados.
- React Native Stallion, Revopush, AppsOnAir — alternativas hospedadas por terceiros criadas especificamente para substituir o CodePush; várias oferecem atualizações de patch binário seguro (enviando apenas os bytes alterados, não todo o pacote) em vez do modelo CodePush que envia o pacote completo a cada vez.
- Servidor CodePush auto-hospedado — a opção de código aberto para equipes que desejam controle total da infraestrutura e já possuem capacidade DevOps para isso.
Por que o suporte a patch/diff é o número que realmente importa: uma atualização OTA com um pacote completo para uma base de usuários grande é realmente cara — um pacote de 8 MB enviado a 500.000 usuários resulta em 4 TB de tráfego de saída para uma única versão. Plataformas que suportam diferenciação binária apenas enviam os bytes alterados, o que é a solução direta e prática para esse custo, sem precisar da complexidade arquitetural do Federation de Módulos.
Onde a OTA atinge seu limite — a mesma restrição que todas as abordagens nesta postagem compartilham: a OTA só pode alterar JS e ativos acessíveis por JS. Qualquer coisa que exija uma mudança nativa — um novo módulo nativo, uma permissão, um pod install ou uma alteração Gradle — ainda precisa de uma versão completa no armazém, independentemente da ferramenta OTA usada.
Escolhendo entre as três abordagens
- Corrigir um bug, enviar uma alteração de cópia ou adicionar uma nova funcionalidade JS apenas rápida para toda a base de usuários → Atualização OTA (EAS Update / Stallion / CodePush auto-hospedado)
- Reduzir o tempo de inicialização e a memória para um aplicativo grande com muitas telas →
React.lazy+Suspensepara avaliação diferida (funciona hoje, sem alteração de ferramentas) - Reduzir o tamanho real do pacote/download ou permitir que uma equipe de recursos envie independentemente do resto do aplicativo → Re.Pack + Module Federation
- Tudo isso, sem trocar os bundlers, uma vez que for lançado → Suporte nativo Metro ao Module Federation — em progresso, ainda não está pronto para produção
Estas não são mutuamente exclusivas. Uma configuração madura geralmente as empilha: OTA para patches rápidos de pacotes completos JS; Module Federation (via Re.Pack) para grandes módulos de recursos independentes que precisam ser entregues em tempo real; e React.lazy/Suspense simplesmente entre eles para ganhos de tempo de inicialização que não exigem infraestrutura adicional alguma.
Resumo
React.lazy + import() dinâmico :
- Web (Vite/Webpack): separa em um bloco carregado pela rede
- React Native (Metro): adia a avaliação, mas não faz uma solicitação de rede
- React Native (Re.Pack): separa em um bloco remoto carregado pela rede
Suspense mecanismo:
- Web (Vite/Webpack): captura a promessa lançada
- React Native (Metro): captura a promessa lançada
- React Native (Re.Pack): captura a promessa lançada — mecanismo idêntico em todos os lugares
Reduz o tamanho do download/binary:
- Web (Vite/Webpack): Sim
- React Native (Metro): Não
- React Native (Re.Pack): Sim
Reduz o custo de execução inicial:
- Web (Vite/Webpack): Sim
- React Native (Metro): Sim
- React Native (Re.Pack): Sim
Permite deploys independentes de equipes:
- Web (Vite/Webpack): com configurações de federação
- React Native (Metro): Não
- React Native (Re.Pack): Sim, via Module Federation
React.lazy e Suspense são as mesmas primitivas do React-core em todos os lugares — o comportamento do reconciler não muda. O que muda por baixo é inteiramente uma decisão do bundler: se essa importação dinâmica resolve para código que já estava no dispositivo, ou para código que realmente foi carregado pela rede pela primeira vez. As atualizações OTA resolvem um problema relacionado mas distinto — enviar um novo pacote inteiro rapidamente, sem revisão da loja de aplicativos — e os dois são complementares, não concorrentes. Entender todos três — avaliação adiada, real divisão em blocos, e entrega OTA — é a diferença entre usar essas ferramentas corretamente e apenas assumir que todas fazem a mesma coisa porque todas terminam com

