Vinicius Quinafelex Alves

🌐English version

[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