Minify HTML in Axum Middleware
Table of Contents
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:
- Only move forward if the
Content-Type
header of the reponse istext/html
- Use
into_parts
method of response to consume the response body give out parts (headers, status code, etc.) and body (actual html content) - Using
to_bytes
function we collect and convertaxum::body::Body
type intoBytes
- Then using
minify_html::minify
function we minify the html - 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.