O cargo-bloat é a ferramenta certa quando um binário Rust ficou grande e você precisa descobrir, em vez de adivinhar, quais funções e dependências estão ocupando espaço. Instale com cargo install cargo-bloat --locked, compile o perfil que realmente será distribuído e rode cargo bloat --release --crates. O relatório agrupa a seção de código por crate; depois, cargo bloat --release -n 20 mostra as funções mais pesadas para orientar o próximo experimento.
O ponto importante é que o cargo-bloat não emagrece o executável sozinho. Ele funciona como um profiler de tamanho: revela onde os bytes estão e permite testar, com comparação antes/depois, decisões como remover features padrão, ativar LTO, trocar uma dependência pesada ou separar uma funcionalidade opcional.
Este guia mostra como analisar tamanho de binário Rust com cargo-bloat, interpretar as colunas do relatório, investigar features e monomorfização, configurar um profile enxuto e evitar otimizações que economizam poucos megabytes enquanto criam risco operacional.
Quando o tamanho do binário Rust importa
Um executável maior nem sempre é um problema. Para uma API em uma VPS com vários gigabytes de disco, reduzir um artefato de 32 MB para 20 MB pode não mudar nada para o usuário. O tamanho passa a ser requisito quando afeta uma parte mensurável da entrega:
- CLIs distribuídas pela internet: downloads menores melhoram instalação e atualização;
- containers: artefatos menores reduzem transferência entre registry, runner e cluster;
- serverless e edge: pacote e cold start podem ter limites apertados;
- embedded: flash e memória são restrições físicas;
- aplicativos desktop: instalador e atualizações diferenciais pesam na experiência;
- agentes e sidecars: dezenas de cópias multiplicam armazenamento e tráfego;
- supply chain: menos dependências e features podem reduzir superfície de manutenção, embora tamanho não seja prova de segurança.
Antes de otimizar, registre pelo menos quatro números: tamanho do arquivo, tamanho compactado, tempo de inicialização e performance da carga principal. Isso evita uma vitória falsa, como reduzir 8 MB e aumentar o uso de CPU em produção.
Para investigar CPU e memória em runtime, o caminho é outro: veja o guia de profiling de Rust em produção. O cargo-bloat responde principalmente a quem ocupa espaço no executável.
O que é cargo-bloat
O cargo-bloat é um subcomando do Cargo que examina o artefato compilado e relaciona partes da seção de texto — o código de máquina — a símbolos e crates. Na prática, ele ajuda a responder:
- quais funções geram mais código;
- quais crates respondem pela maior parte da seção analisada;
- se uma mudança de feature, profile ou dependência realmente reduziu o artefato;
- se o crescimento veio do código da aplicação ou de uma biblioteca transitiva.
A ferramenta não mede tudo da mesma forma que du ou ls -lh. Um executável também pode conter metadados, símbolos, tabelas, dados estáticos, alinhamento e informações de depuração. Por isso, a soma exibida no relatório pode não ser igual ao tamanho total do arquivo.
Essa diferença não é defeito. Use cada medida para sua pergunta:
| Medida | Pergunta respondida |
|---|---|
ls -lh target/release/app | Qual é o tamanho do arquivo entregue? |
gzip -9 -c app | wc -c | Quanto custa aproximadamente transferir o artefato comprimido? |
cargo bloat --release --crates | Quais crates ocupam mais código? |
cargo bloat --release -n 20 | Quais funções aparecem entre as maiores? |
strip / profile.release.strip | Quanto some ao remover símbolos desnecessários? |
Instalação e primeira análise
Instale a ferramenta usando o próprio Cargo:
cargo install cargo-bloat --locked
cargo bloat --version
Na raiz do projeto, gere uma referência do artefato final:
cargo build --release --locked
ls -lh target/release/minha-aplicacao
cargo bloat --release
Para ver as crates em vez de uma lista longa de funções:
cargo bloat --release --crates
Para limitar a saída às vinte funções mais relevantes:
cargo bloat --release -n 20
Se o workspace tem vários binários, informe o alvo de forma explícita:
cargo bloat --release --bin minha-api --crates
cargo bloat --release --bin meu-worker -n 30
Isso é essencial em Cargo workspaces e monorepos. Comparar a API em um comando e o worker em outro pode revelar que os dois carregam features diferentes da mesma dependência.
Guarde uma baseline reproduzível
Não compare um build debug com um release, nem um build limpo com outro que usa flags diferentes. Registre:
rustc --version --verbose
cargo --version
git rev-parse HEAD
cargo bloat --release --bin minha-api --crates > /tmp/bloat-antes.txt
Depois da alteração:
cargo bloat --release --bin minha-api --crates > /tmp/bloat-depois.txt
diff -u /tmp/bloat-antes.txt /tmp/bloat-depois.txt
O ideal é repetir a análise com o mesmo Cargo.lock, target, profile e toolchain. Se um lado usa x86_64-unknown-linux-gnu e o outro usa musl, você está comparando produtos diferentes.
Como interpretar o relatório
Uma saída do cargo-bloat normalmente inclui tamanho do arquivo, tamanho da seção de texto e linhas atribuídas a símbolos. Os nomes exatos e o formato podem variar conforme a versão e a plataforma, mas a leitura operacional é estável.
File
É a quantidade de bytes associada àquele símbolo no arquivo analisado. Ajuda a ordenar os maiores contribuintes visíveis.
.text
Representa a participação na seção de código executável. É útil para entender quanto código de máquina foi gerado, mas não inclui todos os componentes do arquivo final.
Crate ou symbol name
No modo --crates, mostra a atribuição agrupada por crate. No modo padrão, mostra funções e símbolos — frequentemente com nomes longos por causa de genéricos, closures e monomorfizações.
Não conclua que a primeira crate da lista é “ruim”. Uma runtime async, um parser ou uma biblioteca criptográfica pode ser grande porque oferece a função central do produto. A pergunta correta é: estamos usando essa capacidade, e ela precisa estar neste binário?
Primeiro alvo: features desnecessárias
O maior ganho de baixo risco costuma vir de features. Muitas crates ativam um conjunto conveniente por padrão, mas seu binário usa apenas uma parte.
Considere uma dependência HTTP hipotética:
[dependencies]
reqwest = "0.12"
Se o projeto só precisa de JSON e TLS com rustls, uma configuração mais explícita pode ser:
[dependencies]
reqwest = {
version = "0.12",
default-features = false,
features = ["json", "rustls-tls"]
}
O resultado depende do grafo real. O procedimento seguro é:
- rode
cargo tree -e features; - identifique features padrão e quem as ativa;
- desative apenas o que entende;
- compile e teste todos os targets relevantes;
- compare
cargo bloat --release --cratese o tamanho final; - execute benchmarks e testes de integração.
Use também:
cargo tree -e features -i nome-da-crate
O -i ajuda a descobrir por que uma crate aparece no grafo. Às vezes você remove uma dependência direta, mas outra biblioteca continua puxando a mesma crate transitivamente.
Essa revisão conversa com o guia de Cargo e suas ferramentas essenciais e com práticas de segurança da supply chain em Rust: um grafo menor pode ser mais simples de atualizar e auditar, mas a decisão precisa preservar funcionalidade.
Segundo alvo: profile de release
Depois de cortar features obviamente desnecessárias, ajuste o profile. Uma base comum para priorizar tamanho é:
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = "symbols"
Cada opção tem custo e precisa ser medida.
opt-level = "z"
Pede otimização agressiva para tamanho. Em algumas cargas, "s" ou o padrão de release produz melhor equilíbrio entre velocidade e bytes. Faça benchmark; não existe vencedor universal.
lto = true
Link-Time Optimization permite otimizações entre crates e pode remover código ou chamadas redundantes. O custo costuma ser build e link mais lentos. Em CI, combine a decisão com sccache e com o guia de tempo de compilação em Rust.
codegen-units = 1
Dá ao compilador mais contexto para otimização, mas reduz paralelismo e aumenta o tempo de compilação. É uma escolha de release, não necessariamente de desenvolvimento local.
panic = "abort"
Remove a infraestrutura de unwind em cenários compatíveis e pode reduzir o binário. Em troca, o processo encerra ao ocorrer panic. Bibliotecas, FFI e aplicações que dependem de catch_unwind exigem análise especial.
strip = "symbols"
Remove símbolos que não precisam acompanhar o artefato distribuído. Antes de ativar, defina como sua equipe simboliza crashes e perfis. Uma prática madura é guardar símbolos separadamente como artefato privado da release, em vez de simplesmente apagá-los sem plano de diagnóstico.
Monomorfização: quando um genérico vira muitas cópias
Rust especializa funções genéricas para tipos concretos. Isso entrega performance sem dispatch dinâmico, mas pode gerar várias versões de uma função.
Exemplo simplificado:
fn serializar<T: serde::Serialize>(valor: &T) -> Vec<u8> {
serde_json::to_vec(valor).expect("valor serializável")
}
Se o binário chama essa função para muitos tipos, o compilador pode gerar especializações. O relatório pode exibir nomes parecidos repetidos com parâmetros diferentes.
Não remova genéricos por reflexo. Alternativas como trait objects, enums ou uma camada não genérica podem reduzir código e também piorar performance, ergonomia ou arquitetura. Primeiro confirme que as múltiplas instâncias ocupam espaço relevante.
Quando a suspeita é monomorfização em escala, o cargo-llvm-lines é um complemento útil: ele ajuda a estimar quanto LLVM IR cada função gera. A divisão prática é:
- cargo-bloat: onde estão os bytes no executável;
- cargo-llvm-lines: onde o compilador está gerando muito código intermediário;
- benchmark/profiler: qual impacto a mudança produz em runtime.
Dependência pesada: trocar, configurar ou aceitar?
Depois do relatório, uma crate pode dominar o tamanho. Existem quatro respostas legítimas:
- aceitar: ela implementa uma capacidade central e o tamanho cabe no orçamento;
- configurar: desligar features ou escolher um backend menor;
- isolar: mover uma função rara para outro binário, plugin ou serviço;
- substituir: adotar uma alternativa menor após comparar manutenção e comportamento.
Nunca faça “dependency golf” apenas para ganhar um gráfico. Avalie:
- maturidade e frequência de atualizações;
- histórico de segurança;
- compatibilidade de licença;
- suporte a targets necessários;
- performance e alocações;
- custo de migração;
- qualidade da API e da documentação.
Em uma API Axum, por exemplo, remover a runtime Tokio não é uma otimização realista se todo o serviço depende dela. Já desativar features de protocolos ou codecs que não são usados pode ser um ganho honesto.
cargo-bloat em Docker, musl e cross-compile
Analise o mesmo tipo de artefato que vai para produção. Se você distribui x86_64-unknown-linux-musl, não baseie a decisão apenas no build GNU local.
Com um target instalado, a chamada segue o padrão:
rustup target add x86_64-unknown-linux-musl
cargo bloat --release \
--target x86_64-unknown-linux-musl \
--bin minha-api \
--crates
O ambiente ainda precisa do linker e das bibliotecas corretas. Para pipelines multiplataforma, consulte o guia de cargo-zigbuild e cross-compilation.
Em Docker multi-stage, compare duas coisas:
- o binário produzido pelo builder;
- a imagem final publicada.
Uma imagem de 900 MB pode continuar enorme mesmo com executável enxuto porque a stage final copiou toolchain, cache ou pacotes desnecessários. Use builds Docker otimizados para Rust e cargo-chef para resolver a estrutura do container; use cargo-bloat para investigar o código do executável.
Como colocar um orçamento de tamanho na CI
Não comece bloqueando toda pull request por uma variação de poucos bytes. Compiladores, versões de dependência e linkers podem mudar o resultado. Uma adoção progressiva funciona melhor:
- gere o relatório em releases ou em um job noturno;
- publique tamanho do arquivo, tamanho comprimido e top crates como artefato;
- observe algumas semanas para entender a variação normal;
- defina um orçamento alinhado ao produto;
- bloqueie apenas regressões relevantes e revisáveis.
Exemplo simples para medir o arquivo:
cargo build --release --locked --bin minha-api
bytes=$(stat -c%s target/release/minha-api)
limite=$((25 * 1024 * 1024))
printf 'binário: %s bytes\n' "$bytes"
if [ "$bytes" -gt "$limite" ]; then
echo "O binário ultrapassou o orçamento de 25 MiB"
exit 1
fi
Para investigação, salve também:
cargo bloat --release --bin minha-api --crates \
> cargo-bloat-crates.txt
O número de 25 MiB acima é apenas exemplo. Um firmware, uma CLI e um backend têm orçamentos completamente diferentes. Documente por que o limite existe e quem pode aprovar uma exceção.
Erros comuns ao otimizar tamanho
Medir apenas o build debug
Builds de desenvolvimento contêm decisões diferentes e não representam o artefato distribuído. Use o profile e o target reais.
Confundir strip com remoção de código
strip pode reduzir bastante o arquivo ao remover símbolos, mas não significa que você eliminou dependências ou lógica. Compare o arquivo e a seção de código.
Remover capacidade sem teste de integração
Desligar uma feature pode compilar e ainda quebrar TLS, compressão, banco de dados ou observabilidade em produção. Teste o caminho completo.
Priorizar bytes e ignorar CPU
opt-level = "z" pode economizar espaço e piorar throughput. Rode benchmark da carga principal e meça cold start separadamente.
Comparar targets ou toolchains diferentes
Uma atualização de Rust, LLVM, linker ou libc pode alterar o layout. Para atribuir causalidade, mude uma variável por vez.
Otimizar sem orçamento
Sem requisito, qualquer tamanho parece grande. Defina o objetivo: download, flash, cold start, imagem ou custo de distribuição.
Checklist prático de redução
- medir o binário
--releasedo target realmente distribuído; - registrar
rustc,Cargo.lock, commit e flags da baseline; - rodar
cargo bloat --release --crates; - inspecionar as maiores funções com
-n; - revisar
cargo tree -e features; - remover apenas features comprovadamente desnecessárias;
- testar
opt-level = "s"e"z"com benchmark; - medir LTO e
codegen-units = 1contra tempo de build; - decidir conscientemente sobre
panic = "abort"; - manter uma estratégia para símbolos antes de usar
strip; - validar testes, startup, memória e throughput;
- colocar orçamento na CI só depois de conhecer a variação normal.
cargo-bloat e carreira Rust
Saber usar cargo-bloat é um bom sinal em entrevistas de sistemas, embedded, plataforma e engenharia de performance porque demonstra um processo: medir, localizar, formular hipótese, alterar uma variável e validar regressões.
Uma resposta madura para “como reduziria um binário Rust?” não é “ativaria LTO”. É:
- confirmaria o requisito e o target;
- criaria baseline reproduzível;
- usaria
cargo-bloatecargo tree -e features; - atacaria features e dependências sem função no produto;
- compararia profiles;
- validaria runtime e depuração;
- automatizaria um orçamento razoável.
Esse tipo de experiência aparece em times que entregam CLIs, agentes, segurança, infraestrutura e sistemas embarcados com Rust. Para acompanhar oportunidades, consulte as vagas Rust e as empresas que usam Rust.
Perguntas frequentes
O que é cargo-bloat?
cargo-bloat é uma ferramenta do ecossistema Cargo que analisa o executável compilado e mostra quais funções ou crates ocupam mais espaço na seção de código. Ela ajuda a transformar a pergunta genérica sobre binário grande em uma lista concreta de candidatos para otimização.
Como usar cargo-bloat em um projeto Rust?
Instale e rode:
cargo install cargo-bloat --locked
cargo bloat --release
cargo bloat --release --crates
cargo bloat --release -n 20
Em workspaces, informe --bin, --example ou outro alvo aplicável para analisar o artefato correto.
cargo-bloat reduz o tamanho do binário automaticamente?
Não. A ferramenta apenas mede e atribui espaço a funções e crates. A redução vem depois, ao remover features desnecessárias, substituir dependências pesadas, ajustar o profile de release, ativar LTO, usar panic = "abort" ou separar funcionalidades quando isso fizer sentido para o produto.
Qual é a diferença entre cargo-bloat, strip e cargo-llvm-lines?
cargo-bloat atribui bytes do executável a funções e crates; strip remove símbolos e informações que não precisam acompanhar o artefato final; cargo-llvm-lines ajuda a enxergar quanto código LLVM é gerado, especialmente por monomorfização. As ferramentas respondem a perguntas diferentes e podem ser combinadas.
Devo escolher toda dependência pelo menor tamanho?
Não. Tamanho é apenas um requisito entre segurança, manutenção, performance, compatibilidade e produtividade. Otimize quando houver uma restrição real de distribuição, cold start, imagem, firmware ou armazenamento, e valide testes e benchmarks depois de cada mudança.
Conclusão
O cargo-bloat substitui palpites por evidência. Em poucos comandos, ele mostra se o tamanho do executável está concentrado no código da aplicação, em uma dependência, em features que entraram por conveniência ou em várias especializações genéricas.
O fluxo recomendado é simples: meça o release real, agrupe por crate, investigue funções, revise features, teste profiles e valide o produto inteiro. LTO, strip e panic = "abort" são ferramentas; não são metas. A meta é entregar um artefato que caiba no orçamento sem perder segurança, observabilidade ou performance onde elas importam.
Depois de controlar o tamanho, conecte essa análise ao restante do pipeline: sccache para cache de compilação, mold para link mais rápido, cargo-semver-checks para compatibilidade de API e release engineering em Rust. Um binário pequeno é útil; um binário pequeno, testado e reproduzível é um produto.