Executando verificação de segurança...
1

Arquitetando um App de Inventário Offline-First em React Native: Performance, Concorrência e Escalabilidade com 5.000+ Itens

Desenvolvi para uma empresa um Sistema de Inventário de Ativos, onde fui responsável pela arquitetura e implementação do app mobile utilizado em campo para execução do inventário.

O app precisava lidar com certos desafios:

  • Leitura de código de barras e QR Code
  • Listas grandes (5.000+ itens)
  • Sincronização offline com múltiplos usuários
  • Filtros e buscas locais em tempo real

No início parecia algo simples, mas conforme o projeto evoluiu, surgiram desafios técnicos interessantes principalmente relacionados a performance, concorrência e estabilidade em dispositivos Android de baixo RAM.

Abaixo, compartilho algumas decisões arquiteturais e técnicas que fizeram diferença real.

Stack utilizada

  • React Native com Expo (New Architecture)
  • Drizzle ORM com expo-sqlite
  • Zustand para estado global
  • React Hook Form + Zod para formulários
  • NativeWind para UI
  • FlashList para listas massivas
  • react-native-vision-camera para leitura de códigos QR/BARCODE

Filtros e Buscas Locais

Logo na primeira tela, o usuário visualiza uma lista extensa de ativos que pode ser:

  • Atualizada por sincronização.
  • Filtrada por digitação.
  • Buscada por leitura de código de barras.

O primeiro passo foi substituir a FlatList por FlashList, garantindo melhor virtualização e reaproveitamento de views.
Mas isso não era suficiente.

Se eu fizesse um .filter() em memória a cada tecla digitada, a JS thread sofreria bastante principalmente com milhares de registros.

A solução foi delegar completamente as buscas ao banco
fiz a criação de índices no SQLite para colunas como barcode, name e campos de status.
As buscas passaram a acontecer diretamente no banco. Também apliquei debounce, com um pequeno atraso para que a busca fosse executada, evitando múltiplas chamadas desnecessárias enquanto a pessoa ainda está digitando.
Utilizei paginação com LIMIT/OFFSET, e o resultado foi buscas praticamente instantâneas, mesmo com milhares de registros locais.

Leitura de Código de Barras

Para leitura contínua com boa performance, utilizei react-native-vision-camera.
Ele utiliza processamento mais próximo do nativo e entrega desempenho significativamente superior ao expo-camera em cenários de leitura frequente.
Após a leitura do código, a busca ocorre direto no banco indexado, garantindo retorno em milissegundos.

Upload de Imagens sem Pico de Memória

Cada ativo podia conter fotos que precisavam ser enviadas via URL assinada.
Inicialmente eu convertia a imagem para Base64 antes de enviá-la.
Isso gerava:

  • Aumento de tamanho (~33%)
  • Pico de memória na JS thread
  • Risco real de crash em dispositivos mais simples

A solução foi utilizar upload nativo direto do disco:

await FileSystem.uploadAsync(uploadUrl, uri, {
  httpMethod: 'POST',
  uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
  headers: { 'Content-Type': 'image/jpeg' },
});

O que isso garantiu foi uma leitura direta do arquivo em disco, upload executado fora da JS thread, sem conversão intermediária, melhor aproveitamento de memória e, além disso, o uso de streaming nativo, garantindo maior estabilidade.

Arquitetura Offline-First: Sincronização Incremental e Resolução de Conflitos

Garantir que múltiplos usuários realizassem inventário simultaneamente e offline elevou bastante a complexidade.

Usar o Drizzle ORM com SQLite facilitou bastante a implementação, tanto pela sua simplicidade de uso quanto pelo sistema de migração extremamente simples.

Eu precisava garantir que o aplicativo funcionasse offline e que, ainda assim, os ativos estivessem atualizados.

A arquitetura foi baseada em três pilares:

Delta Sync

Ao voltar a ficar online, o app envia um cursor (updatedAfter) para a API e consome estritamente os deltas apenas dados modificados desde o último sync.

Isso reduziu:

  • tráfego desnecessário
  • tempo de sincronização
  • reprocessamento completo do dataset

Action Queue + Optimistic UI

A UI não é bloqueada aguardando a rede.

Cada alteração:

  • Reflete imediatamente na interface
  • Entra em uma fila local
  • Um listener em background envia os dados em batch ao detectar conexão

O usuário nunca fica esperando a internet para continuar operando.

Tombstones + Last-Write-Wins (LWW)

Implementei a resolução de conflitos baseada em timestamp.
Quando um item é excluído offline, ele recebe uma flag deletedAt (tombstone). No backend, utilizei a estratégia Last-Write-Wins para decidir qual versão prevalece.
Com isso, os conflitos passam a ser resolvidos de forma determinística, garantindo a integridade dos dados mesmo com múltiplos dispositivos modificando os mesmos ativos.

Listas Grandes (5.000+ itens)

A parte em que mais trabalhei foi garantir a performance dessa lista. O uso da FlashList foi importante, mas precisei complementar com outras estratégias:

Paginação no SQLite, evitando carregar tudo de uma vez;

Uso de useCallback no renderItem. Mesmo já utilizando o React Compiler, optei por manter o useCallback para garantir maior previsibilidade no comportamento dos renders.

const renderItem = useCallback(
  ({ item }: ListRenderItemInfo<Asset>) => {
    return <AssetItem data={item} />
  },
  []
);

No AssetItem, eu usei React.memo para evitar re-renderizações desnecessárias quando as props não mudavam, já que, em alguns cenários, apenas um item específico era alterado.

O resultado foi um scroll estável, mesmo com milhares de registros armazenados localmente.

Para mim, ficou claro que o React Native é totalmente capaz de suportar aplicações enterprise complexas, com grandes volumes de dados, operação offline real, upload de mídia e sincronização concorrente.

No fim, o diferencial não está apenas na tecnologia escolhida, mas nas decisões arquiteturais que você toma. Delegar o processamento pesado ao banco, evitar sobrecarga na JS thread, controlar re-renderizações com precisão e sincronizar apenas o necessário fazem toda a diferença.

Essas foram apenas algumas das decisões que tomei. Houve várias outras que também contribuíram para que o app operasse sem problemas.

Espero que isso possa, de alguma forma, gerar algum insight para quem está passando por desafios parecidos.

Carregando publicação patrocinada...