Substituímos Dart por Rust no Processamento de Imagens e o App Ganhou 60% de Performance
Ao mover filtros de imagem pesados de Isolates Dart para um módulo nativo em Rust via FFI, reduzimos o tempo de renderização em 60% e eliminamos travamentos na UI.


Em meados de 2025, recebemos um relatório de bug que nos deixou de queixo caído. Usuários com dispositivos mais antigos, como um Galaxy A52, estavam experimentando travamentos de até quatro segundos ao tentar salvar fotos editadas no nosso editor. O problema não estava no salvamento em disco, mas na aplicação do filtro final de "Alto Contraste" que desenvolvemos em Dart puro. O isolado rodava, mas a sobrecarga de memória e o garbage collector entravam em ação no momento errado, travando a thread principal.
Acreditávamos no dogma do Flutter: "Write once, run everywhere" com performance nativa. A realidade é que, para manipulação intensiva de pixels em loop, o Dart, mesmo com otimizações, bateu no teto. A decisão foi drástica: migrar o núcleo do algoritmo para Rust. Não foi uma reescrita total do app — isso seria suicídio comercial —, mas uma substituição cirúrgica do gargalo. O resultado foi um salto de 60% na velocidade de processamento e a eliminação total dos janks.
Onde o Dart pisou na bola
Nosso editor de imagem utilizava uma operação convolucionada para calcular o contraste local. Em Dart, implementamos isso iterando sobre uma lista de inteiros representando os pixels (formato RGBA 8888). A função recebia um Uint8List, fazia o cálculo e retornava uma nova lista.
O problema surgiu quando o usuário tentava processar fotos de 12 megapixels. O loop em Dart, embora otimizado pelo compilador AOT, sofria com alocações constantes de memória intermediária. O Garbage Collector (GC) do Dart é excelente para UI, mas entra em pane quando você cria milhares de objetos pequenos dentro de um loop de milhões de iterações.
Tentamos a primeira solução óbvia: Isolates. Movemos o processo para uma thread separada. Isso impediu que a UI travasse, mas o tempo de processamento total aumentou. O custo de copiar grandes buffers de memória entre o Isolate principal e o worker (passagem de mensagem) comia qualquer ganho computacional. Estávamos movendo o problema, não resolvendo. O app deixava de travar, mas o usuário esperava 6 segundos pelo resultado — uma experiência ruim de qualquer forma.
Por que Rust e não C++ ou Kotlin?
A dúvida era: para onde correr? Kotlin no Android e Swift no iOS resolveriam, mas duplicar a lógica de negócio complexa em duas linguagens nativas diferentes era um pesadelo de manutenção que queríamos evitar a todo custo. Manter paridade entre um algoritmo em Kotlin e outro em Swift é abrir porta para bugs sutis que só aparecem em produção.
Escolhemos Rust por três motivos técnicos que se provaram acertados. Primeiro, o modelo de ownership e o Borrow Checker garantem segurança de memória em tempo de compilação, sem um Runtime ou GC pausando a thread. Segundo, o ecossistema de crates (pacotes) para processamento de imagem, como a imageproc, é maduro e focado em performance, evitando que reinventemos a roda com ponteiros brutos. Terceiro, a integração com Dart via FFI (Foreign Function Interface) está surpreendentemente estável hoje em dia.
Diferente do que vemos em alguns debates sobre O React Native é Maduro o Suficiente para Um App de Realidade Aumentada de Alta Performance?, onde a comunicação entre JS e Nativo pode ser um gargalo, a ponte FFI entre Dart e Rust permite a manipulação direta de ponteiros de memória. Não há serialização JSON nem "bridge" lenta; passamos o endereço de memória do buffer de pixels e o Rust faz a mágica in-place.
A Arquitetura da Migração via FFI
A implementação não foi um passeio no parque. Tivemos que configurar o flutter_rust_bridge, uma ferramenta que gera automaticamente o código "glue" entre Dart e Rust. A estrutura final ficou assim: o Flutter controla a UI, gerencia o estado e recebe a entrada do usuário. Quando o comando de processamento é acionado, o Dart converte o objeto de imagem em um ponteiro bruto (Pointer<Uint8>).
Do lado do Rust, expusemos uma função #[no_mangle] pub extern "C" process_image(...). Essa função recebe o ponteiro, o tamanho da imagem e os parâmetros do filtro. Aqui, o grande ganho veio do uso de slices inseguros (unsafe) apenas na entrada, e depois manipulando os dados usando estruturas seguras e vetorizadas do Rust.

O maior erro inicial foi tentar retornar um novo buffer do Rust para o Dart. Isso forçava uma nova alocação e cópia. A correção foi processar os dados "in-place". O Dart alocava a memória, emprestava para o Rust, o Rust alterava os valores naquele exato endereço e devolvia o controle. A overhead de cópia caiu para zero. Com isso, o processamento que levava 3.200ms no melhor cenário em Dart caiu para estáveis 1.280ms no Rust, testado em um Pixel 6.
Os Custos Ocultos e o Cálculo de Validação
Nada é de graça. O binário do Android aumentou em cerca de 3.8MB e o iOS em 2.5MB devido à biblioteca padrão do Rust embarcada. Em 2026, com 5G dominando e aparelhos vindo com 128GB mínimos, esse impacto é irrelevante para a maioria dos apps, mas vale a pena anotar se você faz um app utilitário de 2MB.
O tempo de build também sofreu. Compilar Rust para as arquiteturas arm64 e x86_64 (para simulação) adiciona um pipeline extra. Tivemos que ajustar nosso CI/CD. Se você já leu o guia sobre Como Configurar o CI/CD do GitHub Actions para Buildar e Subir na Play Store, sabe que cache de dependências é vital. Adicionar uma etapa de cargo build sem cache adequadamente configurado pode fazer seu deployment subir de 5 para 12 minutos.
Além disso, a curva de aprendizado da equipe foi real. Debugar um panic do Rust que ocorre dentro de uma chamada FFI do Dart não é trivial. O stack trace não se conecta perfeitamente. Tivemos que implementar logs robustos que escreviam em arquivos temporários para conseguir rastrear o erro no lado nativo.
Quando não vale a pena
Não saia correndo para reescrever seu app em Rust amanhã. Se a sua lógica é baseada em I/O (rede, banco de dados local), o Dart é mais do que suficiente. O Isolate funciona bem para parsing de JSON ou cálculos matemáticos esporádicos. O crossover de custo-benefício só acontece quando você tem operações "CPU-bound" contínuas, como processamento de áudio, vídeo, criptografia pesada ou manipulação de imagem.
No nosso caso, a escolha foi justificada pelo ROI (Retorno sobre Investimento) em experiência do usuário. Reduzir o tempo de espera de "4s travado" para "1.2s fluido" impactou diretamente a retenção. O módulo, uma vez escrito, praticamente não precisa de manutenção, pois o tipo de dados de imagem não muda com a mesma frequência que uma API de JSON.
Conclusão: Aprove o que está quebrado, não o que funciona
A maior lição técnica dessa migração não foi que "Rust é mais rápido que Dart". Qualquer linguagem compilada com gerenciamento de memória manual provavelmente venceria o Dart em loops matemáticos pesados. A lição é sobre arquitetura híbrida. O Flutter brilha na composição de UI, mas ninguém disse que você precisa amarrar 100% da sua lógica a ele.
Usar o Dart como orquestrador e delegar a "musculação pesada" para uma linguagem de sistemas via FFI é um padrão que veio para ficar. Nosso app está mais estável, os reviews sobre "lentidão" sumiram do Play Store e ganhamos um módulo de processamento que podemos reaproveitar em um backend futuro se quisermos. Se você tem uma função específica que consome mais de 200ms na thread principal, pare de tentar otimizar o Dart e considere chamar um reforço nativo.

