Service Class vs Service Module
This is just a quick post to get this out of my system. I will further explore this subject in a follow-up post.
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 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
options
was minified toe
-
The “private” function
parseJson
was 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 plan to make a more compelling argument in a follow-up post. 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. ↩