O .NET 10 traz melhorias significativas de performance, mas conhecer as técnicas certas faz toda a diferença. Este artigo reúne 25 dicas práticas para extrair o máximo de suas aplicações.
1. Prefira Span para manipulação de dados em memória
Span<T> permite trabalhar com fatias de memória sem alocações adicionais. Ideal para parsing e manipulação de strings ou arrays.
// Evite: cria substring (alocação)
string texto = "nome:valor";
string valor = texto.Substring(5);
// Prefira: zero alocações
ReadOnlySpan<char> span = texto.AsSpan();
ReadOnlySpan<char> valorSpan = span.Slice(5);
O ganho é especialmente relevante em loops ou operações de alto throughput, onde cada alocação evitada reduz a pressão sobre o Garbage Collector.
2. Use FrozenDictionary e FrozenSet para dados imutáveis
Introduzidas no .NET 8 e otimizadas no .NET 10, as coleções "frozen" são ideais para dados que não mudam após a inicialização.
using System.Collections.Frozen;
// Dados de configuração que nunca mudam
var statusCodes = new Dictionary<int, string>
{
[200] = "OK",
[404] = "Not Found",
[500] = "Internal Server Error"
}.ToFrozenDictionary();
// Lookup significativamente mais rápido que Dictionary regular
var descricao = statusCodes[200];
O custo de criação é maior, mas as leituras subsequentes são significativamente mais rápidas devido às otimizações internas de hash (hashing otimizado para lookup). Os ganhos variam conforme o dataset, então sempre meça no seu cenário.
3. Evite Closures em Hot Paths
Closures podem gerar alocações, especialmente quando capturam variáveis externas. Em código executado frequentemente, isso impacta a performance.
// Evite: closure captura 'multiplicador'
int multiplicador = 10;
var resultado = lista.Select(x => x * multiplicador).ToList();
// Prefira: use um loop simples para evitar a captura
var resultado = new List<int>(lista.Count);
foreach (var item in lista)
{
resultado.Add(item * multiplicador);
}
4. Configure o Garbage Collector para seu cenário
O .NET 10 permite ajuste fino do GC. Para aplicações de alta performance, considere o Server GC com regiões habilitadas.
<!-- No .csproj ou runtimeconfig.json -->
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
</PropertyGroup>
// Ou via código para cenários específicos
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
Para aplicações com picos de alocação conhecidos, GC.TryStartNoGCRegion() pode evitar pausas em momentos críticos.
5. Use SearchValues para buscas repetitivas em strings
SearchValues<T> pré-computa estruturas de busca, tornando operações como IndexOfAny muito mais eficientes.
// Pré-compute uma vez
private static readonly SearchValues<char> Separadores =
SearchValues.Create([' ', ',', ';', '\t', '\n']);
// Use múltiplas vezes
public int ContarPalavras(ReadOnlySpan<char> texto)
{
int count = 1;
int index;
while ((index = texto.IndexOfAny(Separadores)) >= 0)
{
count++;
texto = texto.Slice(index + 1);
}
return count;
}
6. Prefira ValueTask para operações que frequentemente completam sincronamente
Quando um método async frequentemente retorna de forma síncrona (cache hit, por exemplo), ValueTask evita a alocação de Task.
private readonly ConcurrentDictionary<string, Produto> _cache = new();
// ValueTask evita alocação quando há cache hit
public ValueTask<Produto?> ObterProdutoAsync(string id)
{
if (_cache.TryGetValue(id, out var produto))
{
return ValueTask.FromResult<Produto?>(produto);
}
return ObterProdutoDoBancoAsync(id);
}
private async ValueTask<Produto?> ObterProdutoDoBancoAsync(string id)
{
var produto = await _repository.GetByIdAsync(id);
if (produto is not null)
{
_cache.TryAdd(id, produto);
}
return produto;
}
ValueTask não deve ser usado indiscriminadamente. Ele não pode ser awaited múltiplas vezes e pode causar boxing se convertido para Task. Use apenas quando a maioria das chamadas (>90%) completa de forma síncrona.
7. Utilize ArrayPool e MemoryPool para arrays temporários
Reutilizar arrays através de pools reduz drasticamente as alocações em operações de I/O ou processamento de buffers.
public async Task ProcessarArquivoAsync(Stream stream)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
{
ProcessarBloco(buffer.AsSpan(0, bytesRead));
}
}
finally
{
// clearArray: true evita vazamento de dados entre usos
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
}
}
Os Arrays retornados pelo pool não são zerados por padrão. Se o buffer contiver dados sensíveis, sempre use clearArray: true no Return para evitar vazamento de informações entre usos.
Para cenários mais complexos, considere MemoryPool<T> que trabalha com Memory<T> e permite maior flexibilidade.
8. Implemente ISpanParsable para tipos customizados
O .NET 10 favorece parsing baseado em Span. Implemente ISpanParsable<T> para seus tipos de domínio.
public readonly record struct Cpf : ISpanParsable<Cpf>
{
private readonly long _valor;
public static Cpf Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
{
// Remove pontuação sem alocar
Span<char> digitos = stackalloc char[11];
int pos = 0;
foreach (char c in s)
{
if (char.IsDigit(c) && pos < 11)
digitos[pos++] = c;
}
if (pos != 11)
throw new FormatException("CPF deve ter 11 dígitos");
return new Cpf(long.Parse(digitos));
}
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out Cpf result)
{
// Implementação similar com tratamento de erro
}
}
9. Use StringComparison explícito
Comparações de string sem especificar o tipo usam cultura corrente, que é mais lento e pode causar bugs sutis.
// Evite: usa CurrentCulture implicitamente
bool igual = str1.Equals(str2);
bool contem = texto.Contains("busca");
// Prefira: significativamente mais rápido para comparações ordinárias
bool igual = str1.Equals(str2, StringComparison.Ordinal);
bool contem = texto.Contains("busca", StringComparison.OrdinalIgnoreCase);
// Para dicionários
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
10. Configure Connection Pooling adequadamente
Conexões de banco de dados são recursos caros. Configure o pool de acordo com sua carga.
// Para SQL Server / PostgreSQL
var connectionString = new NpgsqlConnectionStringBuilder
{
Host = "localhost",
Database = "mydb",
Username = "user",
Password = "pass",
// Pool settings
MinPoolSize = 10,
MaxPoolSize = 100,
ConnectionIdleLifetime = 300,
ConnectionPruningInterval = 10
}.ToString();
// Para Entity Framework Core
services.AddDbContextPool<AppDbContext>(options =>
{
options.UseNpgsql(connectionString);
}, poolSize: 128);
11. Prefira CompositeFormat para Strings formatadas repetidamente
CompositeFormat pré-compila o formato, evitando parsing repetido em logs ou mensagens frequentes.
// Pré-compile uma vez
private static readonly CompositeFormat LogFormat =
CompositeFormat.Parse("[{0:HH:mm:ss}] {1}: {2}");
// Use múltiplas vezes sem parsing repetido
public void Log(string nivel, string mensagem)
{
var linha = string.Format(null, LogFormat, DateTime.Now, nivel, mensagem);
Console.WriteLine(linha);
}
12. Use Source Generators para serialização JSON
System.Text.Json com source generators elimina reflection em runtime, melhorando performance.
[JsonSerializable(typeof(Produto))]
[JsonSerializable(typeof(List<Produto>))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class AppJsonContext : JsonSerializerContext { }
// Uso
var json = JsonSerializer.Serialize(produto, AppJsonContext.Default.Produto);
var produto = JsonSerializer.Deserialize(json, AppJsonContext.Default.Produto);
13. Evite Async em Métodos que não precisam
O overhead de async/await inclui a criação da máquina de estados. Se o resultado já está disponível, retorne diretamente.
// Evite: async desnecessário
public async Task<int> ObterValorAsync()
{
return await Task.FromResult(42);
}
// Prefira: retorno direto
public Task<int> ObterValorAsync()
{
return Task.FromResult(42);
}
// Para validações antes de operações async
public Task<Resultado> ProcessarAsync(Dados dados)
{
if (dados is null)
return Task.FromResult(Resultado.Invalido);
return ProcessarInternoAsync(dados);
}
14. Utilize Parallel.ForEachAsync para I/O concorrente controlado
Para processamento paralelo de I/O, Parallel.ForEachAsync oferece controle fino sobre concorrência.
public async Task ProcessarUrlsAsync(IEnumerable<string> urls)
{
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 10 // Limite de requisições simultâneas
};
await Parallel.ForEachAsync(urls, options, async (url, ct) =>
{
var conteudo = await _httpClient.GetStringAsync(url, ct);
await ProcessarConteudoAsync(conteudo);
});
}
15. Configure HttpClient corretamente
HttpClient deve ser reutilizado. Use IHttpClientFactory para gerenciamento adequado de conexões.
// Configuração
services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.exemplo.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
MaxConnectionsPerServer = 100,
EnableMultipleHttp2Connections = true
});
// Uso
public class MeuServico(IHttpClientFactory factory)
{
private readonly HttpClient _client = factory.CreateClient("api");
}
16. Use stackalloc para Buffers Pequenos
Para buffers pequenos e de vida curta, stackalloc evita completamente o uso do heap.
public string FormatarCpf(long cpf)
{
Span<char> buffer = stackalloc char[14]; // XXX.XXX.XXX-XX
cpf.TryFormat(buffer.Slice(0, 3), out _);
buffer[3] = '.';
// ... resto da formatação
return new string(buffer);
}
// Com limite de segurança para tamanhos variáveis
public void ProcessarDados(int tamanho)
{
Span<byte> buffer = tamanho <= 256
? stackalloc byte[tamanho]
: new byte[tamanho];
// Processar...
}
17. Prefira Struct Records para DTOs pequenos
Para objetos de transferência pequenos e imutáveis, record struct frequentemente evita alocações no heap.
// Alocado na stack, sem pressão no GC
public readonly record struct Coordenada(double Latitude, double Longitude);
public readonly record struct ResultadoPaginado<T>(
T[] Items,
int Total,
int Pagina,
int TamanhoPagina
);
Evite structs grandes (mais de 16-24 bytes) ou com muitos campos, pois o custo de cópia pode superar o benefício.
18. Use Regex Source Generators
Regex compilado via source generator é mais rápido e compatível com Native AOT.
public partial class Validadores
{
[GeneratedRegex(@"^[\w\.-]+@[\w\.-]+\.\w{2,}$", RegexOptions.IgnoreCase)]
private static partial Regex EmailRegex();
[GeneratedRegex(@"^\d{5}-?\d{3}$")]
private static partial Regex CepRegex();
public static bool ValidarEmail(string email) => EmailRegex().IsMatch(email);
public static bool ValidarCep(string cep) => CepRegex().IsMatch(cep);
}
19. Minimize Boxing com Generics Constraints
Boxing de value types cria objetos no heap. Use constraints genéricos para evitar.
// Evite: boxing implícito
public void Processar(IComparable valor) { }
// Prefira: sem boxing
public void Processar<T>(T valor) where T : IComparable<T> { }
// Para interfaces numéricas (.NET 7+)
public T Somar<T>(T a, T b) where T : INumber<T>
{
return a + b;
}
20. Configure Output Caching em Minimal APIs
O .NET 10 traz melhorias significativas no Output Caching para Minimal APIs.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("PorUsuario", builder => builder
.SetVaryByHeader("Authorization")
.Expire(TimeSpan.FromMinutes(1)));
});
var app = builder.Build();
app.UseOutputCache();
app.MapGet("/produtos", async (AppDbContext db) =>
await db.Produtos.ToListAsync())
.CacheOutput(x => x.Expire(TimeSpan.FromMinutes(10)));
app.MapGet("/perfil", async (HttpContext ctx, AppDbContext db) =>
await db.Usuarios.FindAsync(ctx.User.GetId()))
.CacheOutput("PorUsuario");
21. Use Channels para Produtor-Consumidor
Channel<T> é mais eficiente que BlockingCollection para cenários async de produtor-consumidor.
public class FilaDeProcessamento
{
private readonly Channel<Trabalho> _channel = Channel.CreateBounded<Trabalho>(
new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = false
});
public async ValueTask EnfileirarAsync(Trabalho trabalho, CancellationToken ct = default)
{
await _channel.Writer.WriteAsync(trabalho, ct);
}
public async Task ProcessarAsync(CancellationToken ct)
{
await foreach (var trabalho in _channel.Reader.ReadAllAsync(ct))
{
await ProcessarTrabalhoAsync(trabalho);
}
}
}
22. Prefira AsNoTracking para Queries somente leitura
O Entity Framework mantém tracking de entidades por padrão, o que consome memória e CPU.
// Para queries de leitura
var produtos = await _context.Produtos
.AsNoTracking()
.Where(p => p.Ativo)
.ToListAsync();
// Configure globalmente para contextos de leitura
services.AddDbContext<ReadOnlyDbContext>(options =>
{
options.UseNpgsql(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});
23. Utilize StringBuilder com capacidade inicial
Para concatenação de strings em loops, StringBuilder com capacidade pré-definida evita realocações.
public string GerarRelatorio(IReadOnlyList<Venda> vendas)
{
// Estime a capacidade: ~50 chars por linha
var sb = new StringBuilder(vendas.Count * 50);
foreach (var venda in vendas)
{
sb.Append(venda.Data.ToString("dd/MM/yyyy"));
sb.Append(" - ");
sb.Append(venda.Cliente);
sb.Append(": R$ ");
sb.AppendLine(venda.Valor.ToString("N2"));
}
return sb.ToString();
}
24. Habilite Dynamic PGO
Profile-Guided Optimization dinâmica permite que o JIT otimize baseado no comportamento real da aplicação.
<PropertyGroup>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
Ou via variável de ambiente:
DOTNET_TieredPGO=1
DOTNET_TC_QuickJitForLoops=1
DOTNET_ReadyToRun=0
O Dynamic PGO pode trazer ganhos de 10-30% em aplicações de longa execução após o warm-up.
25. Meça ANTES DE OTIMIZAR
A dica mais importante: use ferramentas de profiling para identificar gargalos reais.
// BenchmarkDotNet para microbenchmarks
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net100)]
public class MeusBenchmarks
{
[Benchmark(Baseline = true)]
public void AbordagemOriginal() { }
[Benchmark]
public void AbordagemOtimizada() { }
}
Ferramentas essenciais para profiling:
- dotnet-counters: Métricas em tempo real de GC, threadpool, exceções
- dotnet-trace: Coleta traces para análise detalhada
- dotnet-dump: Análise de memória e diagnóstico de leaks
- PerfView: Análise profunda de CPU e alocações
- Visual Studio Profiler: Integração completa com debugging
# Monitorar métricas básicas
dotnet-counters monitor --process-id <PID> --counters System.Runtime
# Coletar trace de 30 segundos
dotnet-trace collect --process-id <PID> --duration 00:00:30
Conclusão
Performance em .NET 10 é resultado de escolhas conscientes em cada camada da aplicação. Desde a escolha de estruturas de dados apropriadas até a configuração adequada de infraestrutura, cada decisão impacta o resultado final.
Lembre-se: otimização prematura é a raiz de todo mal. Meça primeiro, identifique os gargalos reais, e então aplique as técnicas apropriadas. As dicas apresentadas aqui são ferramentas no seu arsenal, ou como eu costumo chamar... seu cinto de utilidades, use-as quando os dados indicarem necessidade.
Top comments (0)