DEV Community

denisx
denisx

Posted on • Edited on

Reduce bundle size via one-letter css classname hash strategy

Improving bundle compression to 40% of filesize via change standard css classname hash for splitting to one-letter name strategy and filepath.

We develop a web site based on React library, for the styles we use css-modules. The main idea of the css-modules is that you can create components Bird and Cat, they both have styles.css and a css class .block inside it, this css class will be different for each component.

Sample files:

/* Bird / styles.css */
.block { }
.name { }
/* Cat / styles.css */
.block { }
.name { }
Enter fullscreen mode Exit fullscreen mode

It is not a rocket science: in webpack we process our styles with css-loader having localIdentName: "[hash:base64:8]" settings. All classes will be renamed and mapped with source to understand who is who. In the simplest case, we have bundle.css for the styles and styles.js for classnames mapping [for hydrating].

We have 2 independent classnames with strange names such as k3bvEft8:

/* Bird */
.k3bvEft8 { }
.f2tp3lA9 { }
/* Cat */
.epIUQ_6W { }
.oRzvA1Gb { }
Enter fullscreen mode Exit fullscreen mode

Let's run production build and compress files. We will see that, for example, a 300Kb css-file will be packed to 70Kb with gzip [or 50Kb with brotli] It is because the generative hash is not compressed. The compression algorithm can't find any sequence, and remembers every single bytes' position.

What can we do with this? During its work, webpack reads files' tree asynchronously and parses classnames. Your plugin get file path and classname from any of file, but the order inside single file will be kept. If we can catch file name and we know file path, we can make our hash ourselves. Let's make our css classes having just one symbol names instead of hash names. For example use a 52-bits encoding, if we take [a-zA-Z]+ symbols. (Or 64-bits for [a-zA-Z0–9_-]+, but be careful with the first number at name  -  you need a shielding prefix for it, such as '_')

Names will be changed to:

/* Bird */
.a { }
.b { }
/* Cat */
.c { }
.d { }
Enter fullscreen mode Exit fullscreen mode

It seems to be good, but when you run this with webpack config for server, you can get the files in random order [thanks for you, async io/read] and your code can be such as:

/* Bird */
.c { }
.d { }
/* Cat */
.a { }
.b { }
Enter fullscreen mode Exit fullscreen mode

Hello, mismatching!

To fix it, let's try to remember rule entering from every path and save a local count position for every file. Make counting at every file from 0 ['a' symbol]. (51-n rule will named 'Z', 52-n rule will named 'ba', etc)

Get this:

/* Bird */
.a { }
.b { }
/* Cat */
.a { }
.b { }
Enter fullscreen mode Exit fullscreen mode

We have unique names inside every file, but we have many files. We must go to begining and make a hash, but not for classname, for filepath.

Finnaly get:

/* Bird */
.a_k3bvEft8 { }
.b_k3bvEft8 { }
/* Cat */
.a_oRzvA1Gb { }
.b_oRzvA1Gb { }
Enter fullscreen mode Exit fullscreen mode

(You don't need a '_' separator, because a file hash always has fixed length. At this sample for pretty view.)

You get almost the same names as previously, but here we have strongly sequences of classnames. At a sample, from css 50Kb and JS 47Kb we got css 30Kb and JS 28Kb [58Kb sum, br compressed].
Profit ~40Kb and some first paintful performance.

It's simple to write a class to count enteries and call method from webpack config from css-loader, with getLocalIdent.

P.S. If you want to go deeper and make really small names, you can make a css-files tree map before webpack running main thread, sort it, and name such a classnames, to one-lettering [do not forget '_' at this case].

This give you more compression effect: 47Kb for both files and give you a 60Kb profit at unpacked size.

Make web fast again!


P.S. You can see the code at https://github.com/webpack-contrib/css-loader/issues/1028 (PR https://github.com/webpack-contrib/css-loader/pull/1181)

Update: at production site we save 93% from *.css and *style.js. Transferring only 71.6Kb of 1.1Mb unpacked file.

Update 2: Such as we have full map of hash names, we can suggest hashlen (file path hash) for optimal using. For example, at production was set length=8, but we changed it to 4. It benefited us ~14kb for every page (html+js+css). See a PR.


Read my new post Creating simple static server component


Donation: Ethereum 0x84914da79c22f4aC6fb9D577C73E29E4AaAE7622

Top comments (3)

Collapse
 
malstoun profile image
Andrey

Great article! Thank you!

Collapse
 
hurrii profile image
Pavel Silenko

That was helpful. Thanks!

Collapse
 
ilyamilosevic profile image
Ilya Milošević

Thank you, Dan! I'll give it a try at our project ;)