[Web] Técnicas de redução de tráfego
A navegação na web envolve primariamente downloads e processamento de arquivos. A sequência e processo são geridos pelos navegadores. O número de arquivos, o tamanho do conteúdo e as formas como esses arquivos são baixados podem impactar significativamente o tempo de carregamento e a experiência do usuário.
Minificação
A minificação é uma técnica para pré-processar arquivos de texto e reduzir a quantidade de caracteres deles. Normalmente, ela remove espaços em branco, quebras de linha, comentários, renomeia variáveis locais e elimina outros conteúdos que não impactam na experiência do usuário. O total de redução do tamanho do arquivo varia de arquivo para arquivo.
Arquivos de texto estáticos, como CSS, JavaScript e HTML estático, são ótimos candidatos para esse processo, pois podem ser minificados apenas no momento da publicação ou quando o arquivo for devolvido pela primeira vez pelo host, com impacto mínimo na carga de trabalho do servidor de hospedagem.
Arquivos HTML dinâmicos também podem ser bons candidatos, mas a forma como a minificação acontece e quando ela é executada influenciará os trade-offs da solução. Tentar minificar o response de cada request pode ser mais custoso do que retornar alguns bytes extras.
Existem muitas ferramentas disponíveis, incluindo bibliotecas e ferramentas online, que podem minificar arquivos, como minifier.org ou UglifyJS.
A minificação dificulta a leitura dos arquivos, por isso deve ser feita apenas em ambientes de produção ou outra hospedagem. Não é recomendada para uso em ambientes de desenvolvimento, nem deve substituir os arquivos originais em um repositório.
Os exemplos de CSS abaixo demonstram como o processo de minificação altera o conteúdo e, neste caso, reduz o tamanho do arquivo em 43,8% (de 146b para 82b).
/* Unminified CSS */
.square {
display: inline-block;
width: 100px;
height: 100px;
}
.blue {
background-color: #0000ff;
}
.square{display:inline-block;width:100px;height:100px}.blue{background-color:#00f}
Um processo similar ocorre quando se minifica o JavaScript. A maioria dos algoritmos também renomeia nomes longos de variáveis para reduzir a quantidade de caracteres.
O exemplo abaixo demonstra o resultado da minificação do JavaScript, incluindo a renomeação de variáveis. Essa minificação reduziu o tamanho do arquivo JavaScript em 56,8% (de 102b para 44b).
// Comment
function temp(largeVariableName) {
console.log(largeVariableName)
}
temp('test');
function temp(n){console.log(n)}temp("test")
Compressão
A compressão é uma técnica que executa um algoritmo de compressão binária antes de retornar a resposta ao cliente. Em palavras mais simples, é como zipar um arquivo antes de enviá-lo.
Os navegadores informam ao servidor quais tipos de compressão eles são capazes de entender através do atributo Accept-Encoding no cabeçalho do request HTTP, e esperam que o servidor informe o algoritmo utilizado no através do atributo Content-Encoding no cabeçalho do response HTTP.
Alguns dos algoritmos mais comuns, entendidos pela maioria dos navegadores modernos, são gzip e Brotli (br).
Comprimir arquivos de texto é conhecido por reduzir muito seu tamanho, e o mesmo ocorre com o conteúdo web: por exemplo, o tamanho de um arquivo CSS do Bootstrap pode reduzir cerca de 80% quando comprimido com gzip, e outros arquivos de texto podem obter resultados semelhantes.
Se o tamanho do arquivo for muito pequeno (por exemplo, próximo de 1KB), o algoritmo de compressão provavelmente aumentará o tamanho do arquivo em vez de reduzir. Portanto, essa técnica é mais indicada para arquivos maiores.
Note que comprimir o conteúdo requer processamento. Os algoritmos de compressão oferecem configurações para priorizar ou desempenho ou taxa de compressão, por isso é importante considerar os trade-offs entre a o quanto os arquivos devem ser reduzidos e o processamento necessário.
A maioria dos serviços de servidores HTTP, como Nginx, Apache e IIS, oferecem nativamente estratégias de compressão para conteúdo estático e dinâmico, de modo que o software de backend não precise gastar recursos com esse processo.
Bundling
Durante o desenvolvimento, dividir arquivos baseado em características ajuda no processo de criação e manutenção. Mas se a página precisar requisitar muitos arquivos separados, pode impactar e desacelerar a experiência de navegação.
Isso ganha mais significância quando há uso de bibliotecas e pacotes front-end, normalmente distribuídos por sistemas gerenciadores como npm, que facilmente pode adicionar centenas ou milhares de arquivos no projeto.
Bundling é uma técnica que concatena diferentes arquivos do mesmo formato em arquivos-pacote (até em um único arquivo), chamados "bundles". Assim o navegador precisa requisitar menos arquivos para completar o download de uma página.
O desenvolvedor deve avaliar os trade-offs sobre qual a melhor estratégia de bundling, quantos bundles terá e como melhor utilizar o cache em conjunto com eles.
Há ferramentas com diferentes estratégias para bundling, como webpack para módulos javascript, e até mesmo TailwindCSS para permitir que ele adicione todas as estilizações em um único arquivo.
Cache e cache busting
Na web, cache é um recurso antigo que salva os arquivos baixados dentro da máquina, para que o navegador reutilize o mesmo arquivo na próxima vez que ele for necessário, evitando requisições adicionais.
Isso otimiza a capacidade do servidor e acelera o carregamento das páginas quando acessadas recorrentemente. Porém, quando o desenvolvedor desejar atualizar o conteúdo do arquivo, o navegador pode ter problemas por só utilizar o arquivo em cache e não baixar o arquivo atualizado.
O header HTTP Cache-Control pode ajudar a estabelecer um tempo limite para o cache, mas uma forma mais customizada de lidar com essa situação são as técnicas de cache busting.
O Cache dos navegadores é sensível aos parâmetro de path e query string da URL da requisição. Portanto, o link do arquivo ter valores diferentes de query string força o navegador a baixar o arquivo novo ao invés de utilizar cache.
Quando o desenvolvedor quiser garantir que um arquivo esteja atualizado, ele pode alterar o valor da query string, forçando o navegador a requisitar o arquivo do servidor novamente, e esse novo arquivo também será cacheado.
Exemplo ASP.NET
O exemplo abaixo demonstra como usar minificação de conteúdo estático, compressão de conteúdo dinâmico, bundling de javascript/CSS e cache busting.
Considere que os serviços de hospedagem HTTP, Web Application Firewalls, Gateways e outras tecnologias podem oferecer essas funcionalidades de fábrica. Considere os trade-offs entre portabilidade do backend e remoção de responsabilidades do backend.
A biblioteca utilizada para estas features é a LigerShark.WebOptimizer.Core, que permite a maioria das funcionalidades listadas no artigo. Não foi implementado minificação de HTML dinâmico pela compressão já atingir os ganhos significativos com baixo custo de processamento.
Se atente ao fato que o processo de bundling descrito irá empacotar todos os arquivos CSS e javascript, incluindo os não utilizados, e produzirá um único arquivo com todo CSS e um único arquivo com todo javascript. Mantenha a pasta de recursos limpa, e não use essa solução se o arquivo final se tornar muito grande.
O C# record abaixo conterá a chave para ativar ou desativar cada feature, facilitando para o desenvolvedor manter elas desativadas em seu ambiente de desenvolvimento e facilitar a depuração.
// Activate or deactivate features on development or production
public record MinificationSettings(bool Compress, bool Minify, bool Bundle) { }
Configuração do arquivo Program.cs
using Microsoft.AspNetCore.ResponseCompression;
using System.IO.Compression;
// [...]
// Recommended to load from launchSettings or appSettings
var minificationConfig = new MinificationSettings(
Compress: true,
Minify: true,
Bundle: true);
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(minificationConfig);
if (minificationConfig.Compress)
{
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
}
// Always active for cache busting compatibility
builder.Services.AddWebOptimizer(pipeline =>
{
if (minificationConfig.Minify && !minificationConfig.Bundle)
{
pipeline.MinifyCssFiles();
pipeline.MinifyJsFiles();
}
else if (minificationConfig.Bundle)
{
pipeline
.AddJavaScriptBundle("/js/bundle.js", "js/**/*.js")
.AddResponseHeader("content-type", "text/javascript");
pipeline
.AddCssBundle("/css/bundle.css", "css/**/*.css");
}
});
// [...]
var app = builder.Build();
if(minificationConfig.Compress)
app.UseResponseCompression();
// Always active for cache busting compatibility
app.UseWebOptimizer();
// [...]
app.Run();
Views podem ser configuradas para alternar entre carregamento de arquivos individuais ou bundles.
@using YourNamespace
@model MinificationSettings
<html>
<head>
@if(Model.Bundle)
{
<link rel="stylesheet" href="css/bundle.css" />
<script type="text/javascript" src="js/bundle.js"></script>
}
else
{
<link rel="stylesheet" href="css/file1.css" />
<link rel="stylesheet" href="css/file2.css" />
<script type="text/javascript" src="js/file3.js"></script>
<script type="text/javascript" src="js/file4.js"></script>
}
</head>
<body>
<!-- Content -->
</body>
</html>
Cache busting é ativado automaticamente ao importar o WebOptimizer no arquivo _ViewImports.cshtml.
@addTagHelper *, WebOptimizer.Core