Vinicius Quinafelex Alves

🌐Ler em português

[Web] Traffic reduction techniques

Web navigation primarily involves downloading and handling files, a process managed by web browsers. The number of files, the size of content, and the ways those files are downloaded can greatly impact load time and user experience.

Minification

Minification is a technique used to pre-process text files in order to reduce character count. It usually removes blank spaces, line breaks, comments, renames local variables, and eliminates other content that does not improve the user experience. The file size reduction rate varies from file to file.

Static text files, such as CSS, JavaScript, and static HTML, are great candidates for this process because they can be minified only during publishing or when serving the file for the first time, with minimal impact on the workload of the host server.

Dynamic HTML files can also be interesting candidates, but how the minification happens and when it executes will influence the trade-offs of the solution. Attempting to minify the response for every request may become more costly than simply returning a few extra bytes.

There are many tools available, including libraries and online tools, that can apply minification, such as minifier.org or UglifyJS.

Minification makes text files much harder to read, so it should only be done on deployment or hosting environments. It is not recommended for use in a development environment, nor should it replace the original files in a repository.

The CSS examples below demonstrate how a minification process changes the content, and in this case, reduce the file size by 43.8% (from 146b to 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}

A similar process happens when minifying JavaScript. Most algorithms will also rename long variable names to reduce character count.

The example below demonstrates the JavaScript minification result, including variable renaming. This minification reduced the JavaScript file size by 56.8% (from 102b to 44b).

// Comment
function temp(largeVariableName) {
    console.log(largeVariableName)
}

temp('test');
function temp(n){console.log(n)}temp("test")

Compression

Compression is a technique that executes a binary compression algorithm before returning the response to the client. In simpler words, it is like zipping a file before sending it.

Browsers inform the server what kinds of compression algorithms they are able to understand in the HTTP request header Accept-Encoding and expect the server to inform them of the algorithm used in the HTTP response header Content-Encoding.

Some of the most common algorithms, understood by most modern browsers, are gzip and Brotli (br).

Compressing text files is known to greatly reduce their size, and the same applies to web content: for example, a Bootstrap CSS file can shrink by ~80% when compressed with gzip, and other text files may have similar results.

If the file size is very small (for example, close to 1KB), the compression algorithm will likely increase the file size instead of reducing it, so this technique is better suited for larger files.

Be mindful that compressing content requires processing power. Compression algorithms offer configurations to prioritize either performance or compression rate, so it is important to consider the trade-offs between reducing file size and consuming processing power.

Most HTTP server services, such as Nginx, Apache, and IIS, offer built-in compression strategies for static and dynamic content, so the backend software remains unburdened.

Bundling

From a development perspective, splitting files based on different purposes helps with the development and maintenance process. However, requesting lots of separate files may slow down the browsing experience.

This becomes even more significant with the usage of front-end libraries and packages, distributed through managers such as npm, which easily can add hundreds or thousands of files into a project.

Bundling is a technique that concatenate multiple files with the same format into bundles files (or even a single file), so the browser can request fewer files to completely download the entire page.

The developer should evaluate the trade-offs about what is the best bundling strategy, how many bundles and how to best make use of caching with them.

There are a tools with distinct bundling strategies, such as webpack for javascript modules, or adopting TailwindCSS to make it automatically bundle the styles into a single file.

Cache and cache busting

In the web browsing context, caching is an old feature that keeps a downloaded file into the device, so the next time the user access needs to access the files content, the browser can load it from the device instead of requesting the server again.

This optimize server resources and speed up loading time for recurrent users. However, history shows that it can cause problems for developers when they want to change the content of a file, and the client does not receive the update because the device is using a cached (and older) version of the file.

HTTP response header Cache-Control can help managing cache in a time-basis, but a more fine-grained approach would be using cache busting techniques.

Web browser caching is sensible to the path values and query strings of the request. For example, if the device has a certain file cached and the page requires that file but add a query string into the request, then the browser is forced to download the file instead of using the cache.

This leads to cache busting. When the developer wants to make sure certain files are not cached, they can change the value of the query string. Now the client will be forced to request the file again, ensuring that the client will be updated and begins using the new file as cache.

ASP.NET example

The example below demonstrates how to use static content minification, dynamic content compression, javascript/CSS bundling and cache busting.

Note that HTTP server services, Web Application Firewalls, Gateways and other technologies can offer those built-in features. Consider the trade-offs between backend portability and backend performance.

Library used is LigerShark.WebOptimizer.Core, which enables most of the features listed. Dynamic HTML minification was not implemented, because content compression already offers substantial gains with minimal overload.

Be mindful that the bundling process will bundle up all CSS and javascript files, even unused ones, will produce a single CSS and a single javascript file. It is important to keep the folder clean, and avoid this solution if the final file sizes become too big.

The C# record below contains the configuration to toggle each feature on and off, so the developers can keep it off and debug easier on the development environment.

// Activate or deactivate features on development or production
public record MinificationSettings(bool Compress, bool Minify, bool Bundle) { }

Configuration of the Program.cs file.

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 can be configured to toggle between loading each file individually or a single bundle.

@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 activates automatically by adding the WebOptimizer import into the _ViewImports.cshtml file.

@addTagHelper *, WebOptimizer.Core