Favor functions over classes for better minification<!-- --> | <!-- -->Web Performance Tips

Favor functions over classes for better minification

As part of the ES2015 specification, JavaScript received support for the class syntax. This syntax provides familiar object oriented patterns for authoring JavaScript web apps.

Furthermore, with popular transpilation tools like Babel and TypeScript, the JavaScript class syntax has become even more accessible, as it can be easily downleveled for older browsers.

Before you start authoring more JavaScript classes in your codebase, you should understand the performance impact they will have on your web app via bloating resource and transfer sizes.

In this tip, we'll discuss why using JavaScript class syntax may increase your final payload size, and why you are likely better off using ordinary function blocks.

Prerequisites

An Example Class

Let's start by writing an example class, called DataFetcher:

// File DataFetcher.js

class DataFetcher {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }

    fetchSomething() {
        return fetch(`${baseUrl}/something.json`);
    }

    fetchSomethingElse() {
        return fetch(`${baseUrl}/somethingElse.json`);
    }
}

This class is pretty straightforward -- it exposes methods for acquiring data from a remote endpoint.

We would use it in code like this:

import { DataFetcher } from './DataFetcher';

// ...

const dataFetcher = new DataFetcher('https://www.webperf.tips');

const something = await dataFetcher.fetchSomething();

Minifying DataFetcher

If you applied a standard Terser minifier to our code, it would minify like this:

class a{constructor(e){this.baseUrl=e}fetchSomething(){return fetch(`${baseUrl}/something.json`)}fetchSomethingElse(){return fetch(`${baseUrl}/somethingElse.json`)}}

If we applied beautification to this to make it more readable, it would look like this:

class a {
    constructor(e) {
        this.baseUrl = e
    }
    fetchSomething() {
        return fetch(`${baseUrl}/something.json`)
    }
    fetchSomethingElse() {
        return fetch(`${baseUrl}/somethingElse.json`)
    }
}

Notice, that there is minimal difference from our original code. Notably:

  • The DataFetcher class gets renamed to a
  • The constructor argument baseUrl gets minified to e

The Problem

Ordinary minifiers like Terser only apply safe transformations to source code.

Mangling and Compression

Code that utilizes class prevents a minifier from safely mangling or compressing:

  • Members on this within a class. In this example, this.baseUrl.
  • Any method name. In this example, fetchSomething() and fetchSomethingElse()

Tree shaking

Furthermore, minifiers cannot tree shake methods within a class. This is because tree shaking operates at the import and export level.

While tree shaking can shake out entire unused classes, it cannot shake out unused methods within classes.

As a result, a class with many methods will not only minify poorly, but also likely bring in extraneous methods into the final payload.

The Solution: Functions

If you can, try to use export function instead of classes. Let's convert the above example into something utilizing ordinary function blocks:

// File DataFetcher.js

export function fetchSomething(baseUrl) {
    return fetch(`${baseUrl}/something.json`)
}

export function fetchSomethingElse(baseUrl) {
    return fetch(`${baseUrl}/somethingElse.json`)
}

Let's consider the above JavaScript code in the following snippet:

import { fetchSomething } from './DataFetcher';

// ...

const something = await fetchSomething('https://www.webperf.tips');

If we applied Terser-based resource minification, we would see the following results for DataFetcher:

function a(n){return fetch(`${n}/something.json`)}

Applying beautification to make it slightly more readable, we see:

function a(n) {
    return fetch(`${n}/something.json`)
}

Notice how it's significantly smaller than our class based DataFetcher!

Notably:

  • The unused fetchSomethingElse function has been removed via tree shaking
  • The function name fetchSomething is mangled / compressed to a
  • The function argument is mangled / compressed to n
  • There is no un-minifiable assignment to this, like this.baseUrl

A note on Closure

The Closure Compiler in Advanced mode is able to minify classes more aggressively.

With proper Closure minification, our DataFetcher class would have its members and methods all minified. That said, it has more onboarding overhead and requires strict source code-level rules that must be followed.

In my experience, onboarding to Closure is more trouble than simply using function exports, and the results produced are similar.

Conclusion

Although using JavaScript class is familiar and convenient, overusing class can lend itself to large, poorly minified payloads containing extraneous code.

If possible, author JavaScript using export function blocks to maximize your minifier's potential.

That's all for this tip! Thanks for reading! Discover more similar tips matching JS Optimization.