This is just a quick post to get this out of my system. I further explore this subject in the follow-up post “Classes Considered Harmful”.

This is a very simple service class:

export class Chirpy {
  constructor(options) {
    this.options = options;
  }

  chirp(message) {
    return fetch(`${this.options.endpoint}/chirp`, {
      method: 'POST',
      body: JSON.stringify(message),
    })
    .then(this._parseJson);
  }

  rechirp(messageId) {
    return fetch(`${this.options.endpoint}/rechirp`, {
      method: 'POST',
      body: messageId,
    })
    .then(this._parseJson);
  }

  reverseChirp(messageId) {
    return fetch(`${this.options.endpoint}/chirp/${messageId}`, {
      method: 'DELETE',
    })
    .then(this._parseJson);
  }

  _parseJson(response) {
    return response.json();
  }
}

const client = new Chirpy({
  apiKey: 'xyz',
  endpoint: 'example.com',
});

client.chirp('hello world');

This service has a size of 737 bytes.

Going through the terser minifier1 we get this:

export class Chirpy{constructor(e){this.options=e}chirp(e){return fetch(`${this.options.endpoint}/chirp`,{method:"POST",body:JSON.stringify(e)}).then(this._parseJson)}rechirp(e){return fetch(`${this.options.endpoint}/rechirp`,{method:"POST",body:e}).then(this._parseJson)}reverseChirp(e){return fetch(`${this.options.endpoint}/chirp/${e}`,{method:"DELETE"}).then(this._parseJson)}_parseJson(e){return e.json()}}new Chirpy({apiKey:"xyz",endpoint:"example.com"}).chirp("hello world");

It’s now reduced to 482 bytes. 255 bytes fewer. A 34.6% reduction.

It’s worth pointing out the things that do not get minified:

  • Strings: "POST", "DELETE", "xyz", "example.com", "hello world"

  • Object keys: method, apiKey, endpoint

  • Standard built-in objects like JSON and native functions like fetch

  • Native keywords / operators: export, class, new, this

  • Object / Class properties and methods: then, stringify, options, constructor, chirp, rechirp, reverseChirp, _parseJson

This last point is important as we’ll see later.

Let’s now look at the same service implemented as a module:

let options;

export function initialiseChirpy(opts) {
  options = opts;
}

export function chirp(message) {
  return fetch(`${options.endpoint}/chirp`, {
    method: 'POST',
    body: JSON.stringify(message),
  })
  .then(parseJson);
}

export function rechirp(messageId) {
  return fetch(`${options.endpoint}/rechirp`, {
    method: 'POST',
    body: messageId,
  })
  .then(parseJson);
}

export function reverseChirp(messageId) {
  return fetch(`${options.endpoint}/chirp/${messageId}`, {
    method: 'DELETE',
  })
  .then(parseJson);
}

function parseJson(response) {
  return response.json();
}

initialiseChirpy({
  apiKey: 'xyz',
  endpoint: 'example.com',
});

chirp('hello world');

Very similar to the class-based approach. And equal in terms of functionality, except it won’t let you instantiate two different versions of the service.

This module has a size of 692 bytes, a tad smaller than the class’s 737 bytes. Let’s see what happens when we pass it through terser:

let e;export function initialiseChirpy(t){e=t}export function chirp(i){return fetch(`${e.endpoint}/chirp`,{method:"POST",body:JSON.stringify(i)}).then(t)}export function rechirp(i){return fetch(`${e.endpoint}/rechirp`,{method:"POST",body:i}).then(t)}export function reverseChirp(i){return fetch(`${e.endpoint}/chirp/${i}`,{method:"DELETE"}).then(t)}function t(e){return e.json()}initialiseChirpy({apiKey:"xyz",endpoint:"example.com"}),chirp("hello world");

It’s now reduced to 456 bytes. 236 bytes fewer. A 34.1% reduction, similar to the size reduction of the service class. So it seems like there’s not that much of a difference between both approaches.

But notice that:

  • The variable options was minified to e

  • The “private” function parseJson was minified to t

When these were class properties / methods they weren’t minified.2 If your service classes have lots of properties and private methods that could make quite the difference.


Any self-respectable bundler will tree-shake any unused exports. That’s the two unused functions – rechirp and reverseChirp. By tree-shaking the service module we get this:

let i;export function initialiseChirpy(n){i=n}export function chirp(e){return fetch(`${i.endpoint}/chirp`,{method:"POST",body:JSON.stringify(e)}).then(n)}function n(i){return i.json()}initialiseChirpy({apiKey:"xyz",endpoint:"example.com"}),chirp("hello world");

It’s now reduced to 261 bytes. 431 bytes fewer than the initial 692 bytes. A 62.3% reduction.

Let’s encapsulate what we learned in a table format:

size / service Class Module
initial 737 B 692 B
minified 482 B 456 B
tree-shaked* 482 B 261 B

*Assuming only one method / function of the service is used

I believe I make a more compelling argument in the post “Classes Considered Harmful”. But I hope this is enough to at least make you think about your own services.

  1. I used the terser minifier because its REPL is very easy to use and shows the total size of the code. ↩︎

  2. There are ways to minify private properties and methods (see Timokhov’s ts-transformer-minify-privates and ts-transformer-properties-rename, for example). But they are fraught with peril↩︎