Reduce bundle size via one-letter css classname hash strategy
Tags: css, webpack, perfomance, hash
Published: 2019-12-17 19:45:12

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:
```css
/* Bird / styles.css */
.block { }
.name { }
/* Cat / styles.css */
.block { }
.name { }
```

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_:
```css
/* Bird */
.k3bvEft8 { }
.f2tp3lA9 { }
/* Cat */
.epIUQ_6W { }
.oRzvA1Gb { }
```
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:
```css
/* Bird */
.a { }
.b { }
/* Cat */
.c { }
.d { }
```
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:
```css
/* Bird */
.c { }
.d { }
/* Cat */
.a { }
.b { }
```
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:
```css
/* Bird */
.a { }
.b { }
/* Cat */
.a { }
.b { }
```
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:
```css
/* Bird */
.a_k3bvEft8 { }
.b_k3bvEft8 { }
/* Cat */
.a_oRzvA1Gb { }
.b_oRzvA1Gb { }
```
(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](https://dev.to/denisx/creating-simple-static-server-component-39l2)

---

Donation: Ethereum 0x84914da79c22f4aC6fb9D577C73E29E4AaAE7622