DEV Community

Alexandrovich Dmitriy
Alexandrovich Dmitriy

Posted on • Updated on

Extremely reducing the size of NPM packages

One day I wanted to create a small NPM library for all the “best practices” - with test coverage, nice documentation, maintaining valid versioning and changelog, etc. I even wrote an article that described in detail what issues the library solves and how to use it. And one of the tasks that I was interested in when creating it was the task of minimizing the size of the output NPM package - the code that will be used by another programmer. And in this article I would like to describe what methods I applied to in order to achieve the desired goal.

I myself believe that developers of NPM packages should pay special attention to what size of the package they provide to other developers. Because even if the developers of the final product use even the most advanced minifiers, they will not always be able to achieve maximum optimization of the output bundle size, if the developers of NPM packages don't help.

By the way, I'll talk only about those packages that will be bundled into client code. The material below is not relevant for backend packages.

Part one. General advices

I decided to split the article into two parts. In the first one, I will tell you about general tips, e.g. about setting up dependencies or the assembly process. And it's necessary to resort to them.

In the second part I will tell how you can write code to make the package even smaller. And there, some tips will be “extreme” - they can greatly affect on the package size, but also dramatically degrade the developer experience. Therefore, applying those technics is up to you.

Importing third-party packages

Let's start with the simplest and most understandable one. There are a few simple rules on how you should import the code of third-party packages.

First, if you are creating a library in which you are sure that both you and the developer of the final product will use a certain third-party package, you should mark it as an external dependency. For example, if you are developing a UI Kit for React applications, you should mark 'react' as an external dependency.

Example of external dependency configuration in Rollup
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'main.js',

  output: {
    file: 'bundle.js',
    format: 'iife',
    name: 'MyModule'
  },

  plugins: [
    // nodeResolve и commonjs are needed for enabling
    //  the opportunity to import modules.
    nodeResolve(),
    commonjs(),
  ],

  // And here you declare a list of external dependencies
  external: [
    'react',
  ],
};

If a package as marked as an external library it's code wan't be bundle into your package's code.

// If this is your code
import find from 'lodash/find';
export const getPositiveNumbers = (arr: unknown[]) => find(arr, it => it > 0);

// And you marked lodash/find as an external dependency
// This will be your NPM package code
import e from"lodash/find";let o=o=>e(o,e=>e>0);export{o as getPositiveNumbers};

// But if lodash/find is not marked, the content of lodash/find
// will be inserted into your code entirely, so your package
// will grow by 14 KB.
Enter fullscreen mode Exit fullscreen mode

And the second main tip is - there should be a minimum number of imports in your library. More on this below.

Polyfills

It is not necessary to import any polyfills to increase compatibility with different browsers. This, if necessary, will be handled by the developer of the final product. If both you and another developer paste a polyfill, it will be applied twice, and often minifiers will not be able to get rid of such dublicated code.

At the same time, there may be situations when the presence of some polyfills is critically important for the proper functioning of your package. For example, if it uses old version decorators, it is very likely that you will need the 'reflect-medata' package. But even in this case, you don't need to import anything yourself. It is better to point out this need in the documentation of your package, so the developers of the final product will handle it by themselves. Responsibility for importing polyfills should always fall on the shoulders of them.

What to do when you use TypeScript and your package needs typing from a polyfill? For example 'reflect-metadata' extends the typing of the Reflect object. And it may seem that since it is impossible to import a package, then it won't be possible to get typing from it. But no, it is quite possible to import typing without importing JavaScript code.

To do this, it is enough to create a file with the extension *.d.ts, in which you must import the polyfill, and then you must specify this file in your tsconfig.json. And voilà! The necessary typing has appeared in your project, and there will be no code from a third-party package, because files with the extension *.d.ts, in principle, cannot generate JavaScript code.

Example of importing typing without importing JavaScript code

global.d.ts

import 'reflect-metadata';

tsconfig.json

{
  "compilerOptions": {
    ...
  },
  "include": [
    "src",
    // It's important to set your created d.ts file
    "global.d.ts"
  ],
}

Utility packages

The situation with utility packages is a bit more complicated. It is quite logical to use some kind of utility package if it already implements the functionality you need. However, if you don't know how does the Tree Shaking work, how is the imported library assembled, and how do the minifiers work, you can suffer greatly from the overgrown size of your package.

In the example above, I used lodash and got extra 14 KB. However, the situation could become even worse. Take a look at the example below. Functionally, I wrote 2 identical imports. However, in the first case, after import, the size of the packages increases by 70 KB. Almost 5 times more. And all because in the first case I imported the entire library, and in the second only a specific file.

import { find } from 'lodash';
import find from 'lodash/find';
Enter fullscreen mode Exit fullscreen mode

Therefore, I highly recommend you not to use 'lodash' in your NPM packages in 2023. Most of the function it provides are already built in JavaScript.

It is important to know a couple of simple rules about import utility packages.

  1. If the functionality you need is simple, then the easiest way is not to import, but to copy the function and apply the practices described in the second part of the article to it. So, you can not only get rid of potential import overhead, but also further optimize the size of the output file.

  2. Try to use libraries where the Tree Shaking mechanism is available. This mechanism will allow you to delete imported third-party code that is not actually used. To put it simply, every time you write import { … } from 'package'. You are referring to a file that contains all the exported library entities - functions, classes, etc., which means that in reality all these entities get into the final bundle, even if only one function was imported. But thanks to Tree Shaking, unused imports are simply deleted at the compilation stage in the production mode. This mechanism is only available if the package is assembled in ESM format, i.e. using the import/export syntax.

  3. If the package is not assembled in ESM format, try to import only the necessary code, as I did in my example. Lodash acted very humanely and split the functions into separate files. And if you can only import the file you need, then do so.

  4. If you want to use a package that is written in Common JS format (where require is used) and that consists of only one file, then this is a bad package. Don't use it. The developer of this package has not thought about how other developers will use it. Go back to the first point and write or copy the function yourself. But! Of course, we are talking only about those packages where, in addition to the functionality you need, there is unnecessary code there. If you need the whole library, you don't have to suffer like this.

Minifiers

Minifiers are used to reduce the size of the bundle. They can remove unused code, shorten expressions, and so on. And Now there are already several popular minifiers, and they continue to appear: more familiar ones - written in JavaScript - Terser and UglifyJS, even Babel has its own version of the minifier, there are also more modern SWC (written in Rust) and ESBuild (written in Go), and a bunch of other lesser-known minifiers. And I recommend you to look at this repository. It contains up-to-date test results of various popular minifiers.

And here is a brief description of these tests.

  • Different modifiers can provide different level of optimisation. The difference in top 5 is on average 1-2%, but in total the difference can reach 10% between different minifiers.

  • The difference in the speed of the minifiers can differ dramatically - hundreds of times. However, I will not talk about the speed of work in this article - now we only need the quality of compression.

Different minifiers can give different results on your project. If you really want to achieve the minimum file size, you can run the tests yourself and find the best minifier for yourself.

As for myself and as for now, I prefer using SWC's minifier. It can convert const into let, it doesn't add extra braces by default as Terser does, it can inline your variables and so on.

By the way, speaking of extra braces. If you didn't know, terser and some other tools add such extra braces, because OptimizeJS benchmark showed, that it affects on the speed of javascript code parsing. But later, V8 developers describe such technics destructive. And also the main developer of OptimezeJS abandoned his project. So, if you notice your tools generate extra braces - try to remove it.

EcmaScript version

EcmaScript features can be divided into 2 groups - those that add new objects or expand their API, and those that change the syntax of the language. Here is another repository, it conveniently contains all the ECMAScript features by year with their description and examples. If you look at the ES2017 update, then the first group of features would contain the Object.values and Object.entries features, and the second group - asynchronous functions.

Interestingly, the backward compatibility support in older browsers for these features is implemented differently in different groups. For the features of the first group, you need to add polyfills, and as mentioned above, the developer of the NPM package should not do this. But with the features of the second group, everything is more complicated.

If the old browser sees the async keyword, it will not understand what it is about, no matter what polyfills are used. Therefore, the second group of features must be compiled into the form that the browser perceives.

const func = async ({ a, b, ...other }) => {
    console.log(a, b, other);
};
Enter fullscreen mode Exit fullscreen mode

Compiled code into ES5
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (g && (g = 0, op[0] && (_ = 0)), _) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
var func = function (_a) { return __awaiter(void 0, void 0, void 0, function () {
    var a = _a.a, b = _a.b, other = __rest(_a, ["a", "b"]);
    return __generator(this, function (_b) {
        console.log(a, b, other);
        return [2 /*return*/];
    });
}); };

implementations of the possibility of creating asynchronous functions (__await, __generator) and for the possibility of using the rest operator (__rest). The compiler also wrote additional code so that instead of using the object destructor in the function argument, there was an ES5 compatible syntax.

The compiled code looks like a mess. In order to provide backward capability, compiler generated __await and __generator functions because of presence of the async keyword and __rest function because of rest parameter.

The task of the NPM package developer is to decide which ES version to compile its code into. If you used asynchronous functions, and then compiled the library for ES5, then you will create extra code. If the developer of the final product does the same, the code for asynchronous functions will be added twice. And if, on the contrary, he compiles the library for ES2017, it turns out that in the end your code backward capability in unnecessary, and you could simply use modern syntax with async/await.

“Old Believers” may think that it is still most logical to compile the code into ES5 (or even into ES3), because in this way the developer of the final product may not use tools like Babel if he needs ES5. And, for me, they will be wrong. ES6 (ES2015) is now supported by 98% of browsers; support for those browsers that do not use it is either ending or planned to be completed; and therefore there is now a clear trend away from the rejection of compilation in ES5. At the same time, compiling ES6 —> ES5 can have the most significant impact on the package size in comparison with other transitions, because ES6 has brought a lot of improvements to the syntax of the language. In addition, the ES6 syntax is necessary for the tips described in the second part of the article.

Also, using old syntax can affect your performance, but this take is for another article.

Then what version of ES should we compile the code into? If you are writing code for your own use - for yourself or for the project you are working on - specify the version that is specified in the main project. And for the rest of the developers, I would advise to use ES6. All in the name of compatibility. But don't forget to specify in your documentation which version of ES you are using. Just in case to let other developers understand your package may need additional compilation.

But that's not all. You don't just have to compile code for a specific version. You should also be careful to use the syntax of later versions of ES in comparison with the one you are going for. For example, if you compile a package into ES2015, you should not use features from ES2017, e.g. asynchronous functions.

That's not that scary

Most of the features in the new versions of ES are exclusively "sugar". Instead of asynchronous functions, you can use Promises, instead of Object.entries - Object.keys, instead of Array.prototype.includes - Array.prototype.find. And if there is still no analog of the functionality, you can write it yourself.

// ESNext syntax
const func = async () => {
  const result = await otherFunc();
  console.log(result);
  return result.data;
};

// ES6 syntax
const func = () => {
  return new Promise(resolve => {
    otherFunc().then(result => {
      console.log(result);
      then(result.data);
    });
  });
};

// ==================

// ESNext syntax
if ([1, 2, 3].includes(anyConst)) { /* ... */ }

// ES6 syntax
if (!![1, 2, 3].find(it => it === anyConst)) { /* ... */ }

// ==================

// ESNext syntax
Object.entries(anyObj).forEach(([key, value]) => { /* ... */ });

// ES6 syntax
Object.keys(anyObj).forEach(key => {
  const value = anyObj[key];
  /* ... */
});

It would be important to add, that although with caution, but using polifill based features is allowed. But syntax-based is a big "no-no".

Production/Development assemblies separation

A short but interesting topic. If suddenly you want to leave some functionality that only the developer should see - for example, validation of function parameters, error output to the console, etc. - you should split the assemblies, and leave this additional functionality only in dev assemblies.

A lot of package do it, like React, MobX or Redux Toolkit. And it's actually quite simple to organise.

Source code
export const someFunc = (a: number, b: number) => {
  if (__DEV__) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      console.error('Incorrect usage of someFunc');
    }
  }

  console.log(a, b);
  return a + b;
};

rollup.config.js
import typescript from '@rollup/plugin-typescript';
import define from 'rollup-plugin-define';

export default [false, true].map(isDev => ({
  input: 'src/index.tsx',
  output: {
    file: `dist/your-package-name.${isDev ? 'development' : 'production'}.js`,
    preserveModulesRoot: 'src',
    format: 'esm',
  },
  plugins: [
    typescript(),
    define({
      replacements: {
        __DEV__: JSON.stringify(isDev),
      },
    }),
  ],
}));

Output dev version
const someFunc = (a, b) => {
    {
        if (typeof a !== 'number' || typeof b !== 'number') {
            console.error('Incorrect usage of someFunc');
        }
    }
    console.log(a, b);
    return a + b;
};

export { someFunc };

Output prod version
const someFunc = (a, b) => {
    console.log(a, b);
    return a + b;
};

export { someFunc };

Let's formalize it. It is enough for you at the compilation stage of your code to replace some sequences of characters (in my case __DEV__) with the desired value - true or false. Next, you need to use the created flag in the condition. When the flag is substituted in the code, the conditions if (true) { ... } and if (false) { ... } are obtained. And then the if (false) {... } code is cut off, because it will never be called.

Having two files, you need to somehow substitute them into the assembly of the developer of the final product. Before that, it is enough to refer to the NODE_ENV environment variable in your main package file. At the same time, the developer of the final product does not have to configure this variable when using your package - Webpack, for example, configures it by itself.

And in order to use a valid file, you must add a condition into your main file.

// process.env.NODE_ENV sets on the final project side
if (process.env.NODE_ENV === 'production') {
  // using production version
  module.exports = require('./dist/react-vvm.production.js');
} else {
  // using development version
  module.exports = require('./dist/react-vvm.development.js');
}
Enter fullscreen mode Exit fullscreen mode

In addition, I can also say that it there's no need to minify the dev assembly of the package. It's "dev", therefore the developer can actively interacts with it. And it is extremely difficult to interact with minimized code.

Part two. Worse DX, but better outcome

The developers of the final product can afford to write code so that it is convenient for him - their code base will be larger by default. The developer of the NPM package has a smaller codebase, and therefore it is much affordable to write sometime less "beautiful" code to reduce the final size.

And this is due to the fact that the minifier is not a magic tool. The developer must work with him in tandem - he writes the code in a certain way so that the minifier can squeeze his code even more.

Repeatability and reusability

This is, of course, a general practice, but it also affects the size of the package. There should be no repeatable code.

Function creation

If there are pieces of code in the code that are repeated - partially or fully, try to separate the repeated functionality into a separate function. I think this item does not need an example.

Object's properties

There are also a couple of interesting things with objects. If you use the expression object.subObject.field in the code, the minifier will be able to compress this expression to a maximum of o.subObject.field, because the minifier does not know whether further compression is safe. Therefore, if you often refer to the same field in an object, create a separate variable for it and use it.

Example before optimization

Source code:

import { SomeClass } from 'some-class';

export const func = () => {
  const obj = new SomeClass();
  console.log(obj.subObject.field1);
  console.log(obj.subObject.field2);
  console.log(obj.subObject.field3);
};

Minified code (182 bytes)

For clarity, I added line breaks and indents, but the file size is specified without them.

import {SomeClass as o} from "some-class";

const e = () => {
    const e = new o;
    console.log(e.subObject.field1), console.log(e.subObject.field2), console.log(e.subObject.field3)
};
export {e as func};

Example after optimization

Source code:

import { SomeClass } from 'some-class';

export const func = () => {
  const obj = new SomeClass();
  const sub = obj.subObject;
  console.log(sub.field1);
  console.log(sub.field2);
  console.log(sub.field3);
};

Minified code (164 bytes)

import {SomeClass as o} from "some-class";

const e = () => {
    const e = (new o).subObject;
    console.log(e.field1), console.log(e.field2), console.log(e.field3)
};
export {e as func};

The next optimization can be called extreme since it does worsen DX. If you have to frequently use a specific property or a method in an object, you can create a variable of the name of such property or a method.

Example before optimization

Source code:

import { useEffect, useLayoutEffect, useRef } from 'react';

export const useRenderCounter = () => {
  const refSync = useRef(0);
  const refAsync = useRef(0);

  useLayoutEffect(() => {
    refSync.current++;
  });

  useEffect(() => {
    refAsync.current++;
  });

  console.log(refSync.current, refAsync.current);

  return {
    syncCount: refSync.current,
    asyncCount: refAsync.current,
  };
};

Minified code (254 bytes)

import {useRef as r, useLayoutEffect as e, useEffect as t} from "react";

const n = () => {
  let n = r(0), u = r(0);
  return e(() => {
    n.current++
  }), t(() => {
    u.current++
  }), console.log(n.current, u.current), {syncCount: n.current, asyncCount: u.current}
};
export {n as useRenderCounter};

Example after optimization

Source code:

import { useEffect, useLayoutEffect, useRef } from 'react';

const CURRENT = 'current';

export const useRenderCounter = () => {
  const refSync = useRef<number>(0);
  const refAsync = useRef<number>(0);

  useLayoutEffect(() => {
    refSync[CURRENT]++;
  });

  useEffect(() => {
    refAsync[CURRENT]++;
  });

  console.log(refSync[CURRENT], refAsync[CURRENT]);

  return {
    syncCount: refSync[CURRENT],
    asyncCount: refAsync[CURRENT],
  };
};

Minified code (234 bytes)

import {useRef as e, useLayoutEffect as r, useEffect as t} from "react";

const n = "current", o = () => {
  let o = e(0), u = e(0);
  return r(() => {
    o[n]++
  }), t(() => {
    u[n]++
  }), console.log(o[n], u[n]), {syncCount: o[n], asyncCount: u[n]}
};
export {o as useRenderCounter};

The more times current is used, the more effective this optimization will be.


Using ES6 syntax

Using ES6 syntax in conjunction with a minifier can be a good way to shorten your code.

Using arrow functions

In terms of compressibility, arrow functions are better than classical ones in everything. For two reasons. Firstly, when declaring arrow functions in a row via const or let, all subsequent const or let except the first one are shortened. Secondly, arrow functions can return values without using the return keyword.

Example before optimization

Source code:

export function fun1() {
  return 1;
}

export function fun2() {
  console.log(2);
}

export function fun3() {
  console.log(3);
  return 3;
}

Minified code (126 bytes)

function n() {
    return 1
}

function o() {
    console.log(2)
}

function t() {
    return console.log(3), 3
}

export {n as fun1, o as fun2, t as fun3};

Example after optimization

Source code:

export const fun1 = () => 1;

export const fun2 = () => {
  console.log(2);
};

export const fun3 = () => {
  console.log(3);
  return 3;
}

Minified code (101 bytes)

const o = () => 1, l = () => {
    console.log(2)
}, c = () => (console.log(3), 3);
export {o as fun1, l as fun2, c as fun3};

Object.assign and spread operator

This is a specific case of the general rule described in the first part. There weren't such thing as spread operator in objects in ES6. Therefore, if you are compiling your library under ES6, I would recommend that you use Object.assign instead of this operator.

Example before optimization

Source code:

export const fun = (a: Record<string, number>, b = 1) => {
  return { ...a, b };
};

Minified code (76 bytes)

const s = (s, t = 1) => Object.assign(Object.assign({}, s), {b: t});
export {s as fun};

As you see, Object.assign may apply twice.


Example after optimization

Source code:

export const fun = (a: Record<string, number>, b = 1) => {
  return Object.assign({}, a, { b });
};

Minified code (61 bytes)

const s = (s, t = 1) => Object.assign({}, s, {b: t});
export {s as fun};

Try to return the value in the arrow function

Another optimization from the "extreme" category. If this does not affect the functional component, you can return the value from the function. The savings will be small, but they will be. It works, however, in cases when there is only 1 expression in the function.

Example before optimization

Source code:

document.body.addEventListener('click', () => {
  console.log('click');
});

Minified code (68 bytes)

document.body.addEventListener("click",()=>{console.log("click")});

Example after optimization

Source code:

document.body.addEventListener('click', () => {
  return console.log('click');
});

Minified code (66 bytes)

document.body.addEventListener("click",()=>console.log("click"));

Stop creating variables in functions

Another optimization from the category of "extreme". In general, trying to reduce the number of variables is a normal idea for optimization - the minifier gets rid of them if it can do inline code insertion. However, the minifier cannot get rid of all variables independently for the same security reasons. But you can help him.

Look at the compiled file of your library. If it has functions in the body of which there are some variables, then you can use the function argument in your code instead of creating a variable.

Example before optimization

Source code:

export const fun = (a: number, b: number) => {
  const c = a + b;
  console.log(c);
  return c;
};

Minified code (71 bytes)

const o=(o,n)=>{const t=o+n;return console.log(t),t};
export{o as fun};

Example after optimization

Source code:

export const fun = (a: number, b: number, c = a + b) => {
  console.log(c);
  return c;
};

Minified code (58 bytes)

const o = (o, c, e = o + c) => (console.log(e), e);
export {o as fun};

This is a pretty strong optimization, because in the end it can help get rid not only of const, but also of return keywords in the assembled file. But keep in mind that such optimization should be applied only on private class methods and on functions that are not exported from your library, because you should not complicate the understanding of your library's API with your optimization.

Minimal use of constants

Again, the "extreme" advice. In the assembled code, in principle, there should be a minimum number of uses of let and const. And for this, for example, all constants can be declared in one place one after another. At the same time, the advice becomes extreme only if we try to declare literally all constants in one place.

Example before optimization

Source code:

export const a = 'A';

export class Class {}

export const b = 'B';

Minified code (67 bytes)

const s = "A";

class c {}

const o = "B";
export {c as Class, s as a, o as b};

Example after optimization

Source code:

export const a = 'A';
export const b = 'B';

export class Class {}

Minified code (61 bytes)

const s = "A", c = "B";

class o {}

export {o as Class, s as a, c as b};

General advice for extreme reduction

In fact, you can come up with a lot of tips. Therefore, summarizing, I decided to simply describe how the assembled minified file should look like. And, accordingly, if your output file does not match the specified description, it has a lot to squeeze.

  1. There should be no repetitions. Repetitive functionality can be allocated to functions, frequently used object fields need to be written to constants, etc.

  2. The amount of non-abbreviated code should be kept to a minimum. This includes the frequently repeated use of this, the use of nested objects or methods, etc. It is highly desirable that the minimized file contains exclusively single-letter expressions.

  3. The number of function, return, const, let and other keywords should also be kept to a minimum. Use arrow functions declared via const, declare constants in a row, use arguments instead of declaring a constant in functions, etc.

And the most important thing. It makes sense to resort to extreme reduction only when all other optimizations have already been applied and only if it does not affect the functionality of your package. And also, once again, optimization should not affect the API (and, therefore, typing).

Conclusion

It might seem to you that there is no point in extreme compression, because in my examples I got a maximum gain of a couple dozen bytes. But in fact, I specifically made them minimally representative. In real conditions, the gain can be much greater.

But in the end, whether or not to resort to the use of “extreme” advice is up to you. For me, it was more of a challenge to myself about whether I could achieve the minimum possible file size. But in case you are still wondering how useful they are, I can say that they helped me reduce the size of my library from 2172 bytes to 1594. That on the one hand only 578 bytes, but on the other as much as 27% of the total packet volume.

Thank you for your attention, you can share your opinion in the comments. I hope my article was useful for you - if not extreme advice, then at least general. It is likely that I did not specify something in my article. In this case, I will be happy to supplement it according to your suggestions.

Top comments (1)

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Very interesting, looks like I may need to rethink some of my decisions on package contents given this very enlightening point of view.