Service Class vs Service Module
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
JSONand native functions likefetch -
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
optionswas minified toe -
The “private” function
parseJsonwas minified tot
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.
-
I used the terser minifier because its REPL is very easy to use and shows the total size of the code. ↩
-
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. ↩