How to compress HTML emails
January 04, 2024
Happy New Year and welcome to 2024!
We recently added HTML compression to emails being sent through our SMTP gateway, or processed via our Inbound hooks. Here’s how we did it, and how you can add HTML compression to your emails.
Identify an HTML pre-processor
Our SMTP servers are written in Typescript, so we started by looking through npm and github for a node module that could handle this. We found two contenders:
We found that although #2 was faster in our tests, being written in Rust, #1 was was plenty fast for us already (most HTML emails are quite small), and is written in JavaScript so simpler to add to our stack, so we went with that.
Implement it, DRY style
We wrote a simple module, and added some unit tests to make sure it was behaving as expected:
minify.ts
import { minify, Options } from 'html-minifier-terser';
const minfierOptions: Options = {
collapseWhitespace: true,
removeComments: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
includeAutoGeneratedTags: false,
continueOnParseError: true,
};
export default async function (html: string): Promise<string> {
return await minify(html, minfierOptions);
}
minify.spec.ts
import test from 'ava';
import minify from './minify';
test('minifies html', async (t) => {
const html = ' <table width="100%"> Content </table>';
const minified = await minify(html);
t.deepEqual(minified, '<table width="100%">Content</table>');
});
test('handles invalid html', async (t) => {
// Self-closing tag without '/'
const invalidHtml1 = '<img src="image.jpg">';
t.deepEqual(await minify(invalidHtml1), invalidHtml1);
// Unquoted attribute values
const invalidHtml2 = '<a href=/link>Unquoted Attribute</a>';
t.deepEqual(
await minify(invalidHtml2),
'<a href="/link">Unquoted Attribute</a>',
);
// Invalid characters
const invalidHtml3 = '<div><div>Invalid</div></div>';
t.deepEqual(
await minify(invalidHtml3),
'<div><div>Invalid</div></div>',
);
// Remove comments
const invalidHtml4 = '<!-- Comment --> <p>Text</p>';
t.deepEqual(await minify(invalidHtml4), '<p>Text</p>');
});
Configuring it
By default, the parser starts with everything off, and you turn things on one by one. The options we defined in minifierOptions
above, we found to be a good balance between preserving the HTML as found and compression.
Critically we have continueOnParseError: true
so that when people send us malformed HTML we still allow it through, given how forgiving HTML email clients are.
Using the module
At this point we we can import it to use it in our SMTP server:
import minify from './minify';
...
export async function processEmail() {
const html: string = await minifyHtml(parsed.html);
...
}
And that’s it!
Wrap up
Of course, if you want to take advantage of this without writing a single line of code, just send your emails through MailPace!
By Paul, founder of MailPace. Follow our journey on Mastodon and Twitter, and sign up to our Product Newsletter.