Vivek Shukla

Minify HTML in Axum Middleware

Published on

Minifying HTML is one of the simplest way to save bandwidth and decrease the time in transit. We can easily lower page size by 30-50% by minifying HTML.

We will be using minify-html crate for this. With the use of Axum middleware, minification will be applied to all the eligible routes.

Using axum middleware for HTML minification is template agnostic which means it will work with any template engine such as Askama, Tera, Maud, etc., as long as response type is HTML, it should work fine.

Cargo.toml

Add minify-html to your Cargo.toml.

...
minify-html = "0.16.4"
...

🔗Basic Usage of minify-html

minify-html provides minify function which accepts array of bytes and reference to configuration struct which tells it how to minify.

let cfg = minify_html::Cfg {
        keep_closing_tags: true,
        keep_html_and_head_opening_tags: true,
        keep_comments: false,
        minify_doctype: false,
        minify_css: true,
        minify_js: true,
        ..Default::default()
    };

let mut code: &[u8] = b"<p>  Hello, world!  </p>";
let minified = minify_html::minify(&code, &cfg);
assert_eq!(minified, b"<p>Hello, world!</p>".to_vec());

🔗Creating Axum Middleware

We are using Axum’s axum::middleware::map_response middleware to modify the response on the fly by passing an async function.

Steps to be performed by middleware:

  1. Only move forward if the Content-Type header of the reponse is text/html
  2. Use into_parts method of response to consume the response body give out parts (headers, status code, etc.) and body (actual html content)
  3. Using to_bytes function we collect and convert axum::body::Body type into Bytes
  4. Then using minify_html::minify function we minify the html
  5. Finally build the new response using Response::from_parts with the parts that we extracted earlier and new body (minified html)
use std::sync::LazyLock;
use axum::{
    body::{Body, to_bytes},
    http::header,
    response::Response,
};

static MINIFY_CFG: LazyLock<minify_html::Cfg> = LazyLock::new(|| minify_html::Cfg {
    keep_closing_tags: true,
    keep_html_and_head_opening_tags: true,
    minify_doctype: false,
    minify_css: true,
    minify_js: true,
    ..Default::default()
});

pub async fn minify_html_response(response: Response<Body>) -> Response<Body> {
    let content_type = response
        .headers()
        .get(header::CONTENT_TYPE)
        .map(|h| h.to_str().unwrap_or_default())
        .unwrap_or_default();

    if content_type.contains("text/html") {
        let (parts, body) = response.into_parts();
        let bytes = to_bytes(body, usize::MAX).await.unwrap_or_default();
        let minified = minify_html::minify(&bytes, &MINIFY_CFG);
        let new_response = Response::from_parts(parts, Body::from(minified));
        return new_response;
    }

    response
}

Note: We are using LazyLock to initialize minify_html::Cfg only once, since we are going to need the reference to it for each minify call.

Plugging into the Router:

let router = axum::Router::new()
    .route("/", get(index_page))
    .layer(axum::middleware::map_response(minify_html_response));

🔗Benchmark

I’ve done very basic benchmarking on this

Overhead: The overhead of extracting response body and minifying adds around 600 to 800 µs.

Size Reduction: This will be varied for everyone and depends on the content they serve. However, I’ve consistently seen at least 30% reduction in HTML document size.