How to reduce bundle size - single-letter class strategy in css-modules

We improve the compression of bundles by 40% of the file size, by replacing the standard hashing with a one-letter prefix + hash of the file path.

Css-modules allow you to write Bird and Cat components, with styles in files with the same name styles.css and .block classes in each, and these classes will be different for each of these components.

/* Bird / styles.css */
.block { }
.name { }
/* Cat / styles.css */
.block { }
.name { }

There is nothing tricky here: the webpack hashes each class from all the files using the "[hash: base64: 8]" setting. All classes will be renamed, and links affixed to understand which class came from. In the basic version of the assembly, we will have a styles.css file for styles and styles.js for links when working with js.

Continuing the test case, we get 4 independent classes with strange names like k3bvEft8:

/* Bird */
.k3bvEft8 { }
.f2tp3lA9 { }
/* Cat */
.epIUQ_6W { }
.oRzvA1Gb { }

Run the production assembly and compress the files. At the stand, a 300Kb css file was packaged in 70Kb using gzip [or 50Kb using brotli]. Compression is small because hashes are randomly generated strings that compress very poorly. Compression algorithms do not see sequences and are forced to remember the locations of each symbol, i.e. transfer the contents of these sections as is, without compression.

Something needs to be done with this. But what? During operation, the webpack reads the file tree asynchronously, and also goes through class names. Each time in a different way. The only thing you can cling to is the order of names inside css - it is constant (otherwise everything will break, in css the order is important). The class position number in the file is encoded in a one-letter prefix. You can take encoding in 52 ([a-zA-Z] +) or 64 ([a-zA-Z0-9 _-] +) characters. The main thing here is not to forget to put down a protective prefix in cases with a number or hyphen.

/* Bird */
.a { }
.b { }
/* Cat */
.c { }
.d { }

It seems to look good - the names are compressed as much as possible. But the catch is that the webpack is asynchronous, and each launch, and especially when the server and client simultaneous assemblies are launched in parallel, receives files in a chaotic manner, as do the class names. Thanks for the speed, but here it interferes.

/* Bird */
.c { }
.d { }
/* Cat */
.a { }
.b { }

You see, caught a file order mismatch.

We fix this behavior by remembering the file, where the classes came from, and their position number.

/* Bird */
.a { }
.b { }
/* Cat */
.a { }
.b { }

Saved the order inside the files. But you need to somehow distinguish the files from each other. A hash from the file path will help to avoid confusion.

/* Bird */
.a_k3bvEft8 { }
.b_k3bvEft8 { }
/* Cat */
.a_oRzvA1Gb { }
.b_oRzvA1Gb { }

('_' is not needed here, it is only for illustrative purposes. The hash has a stable length, unlike the prefix, and there can be no collisions)

We have obtained class names that are absolutely unique for the project, but containing repeated sequences.

In our project, from the files css 50 Kb and js 47 Kb we got css 30 Kb and js 28 Kb [58 Kb in total, brotli].
Saving almost 40Kb. The size of critical css and the size of html will slightly decrease.

It remains to write a class for processing data from the webpack and throw a call in the css-loader config (getLocalIdent)

PS You can go further and save the file paths, sort the paths, and also replace them using a single-letter strategy, but this is worse in terms of long-term caching, plus you need to make several passes in the assembly and assemble the client / server sequentially.

PS2 You can try on your project now, if you take the code here

PS3. In production, we compress * .css and * style.js files by 93%. We transfer 71,6Kb from 1,1Mb of the unpacked file (brotli)

All Articles