The Official MailPace Blog

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:

  1. https://github.com/terser/html-minifier-terser
  2. https://github.com/wilsonzlin/minify-html

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!


You've read this far, want more?


By Paul, founder of MailPace. Follow our journey on Mastodon and Twitter, and sign up to our Product Newsletter.