Before diving into Long answer π©βπ«, let see why stylesheet's size is one of the most important factors in term of web experience.
In general, a stylesheet is just bunch of rules/selectors which can be defined in:
- External files.
- Style tags.
- Inline style attributes.
βοΈ There are a lot of discussions for pros and cons of each approach and it's beyond the scope of this article, but if you are interested in you can read here.
In order for a browser to display a webpage, a browser has to construct DOM (our HTML) and CSSOM (our stylesheets) trees like the image below. It means that constructing CSSOM is a render blocking, therefore, the bigger file's size and the more CSS rules we have, the slower our website is π€·ββοΈ .
So, let's start our thinning-the-stylesheet journey. In my opinion, we can classify into two categories:
- "Minifying" a stylesheet.
- Removing unused CSS selectors.
βοΈ I quote minifying because I cannot find a better word to describe what it does in the following section.
What is minifying? In this section, it does two different things:
- Perform transformations (aka CSS Minifier).
- Cutting css's class name.
CSS Minifiers basically does three things:
- Cleaning (removing duplicated/empty rules and comments).
- Compression (shorter form for color, font, background).
- Restructuring (merge of declarations, rulesets and so on).
The image below is the benchmark of CSS minifiers
π Worst - π Average - π Best
As image above shows, there are a bunch of css minifiers, I encourage you use cssnano or csso, top two popular css minifiers in term of features and ecosystem. You can use them in your module bundler via:
- Webpack <-> csso-webpack-plugin, css-loader (enable minimize).
- Rollup <-> rollup-plugin-postcss.
- Gulp <-> gulp-postcss.
- Grunt <-> grunt-postcss.
If you sneak into Gmail HTML page (the same for other Google platforms: G+, Driver ...), you will see something like this
It looks ugly π©, right? because it doesn't have any meaningful class names, we were taught that we should use meaningful names when programming to improve readability and maintainability, it goes against that. Why Google does that? Because shorter class name means:
- smaller size -> fewer bytes to delivery -> our website is faster π.
- faster when querying rule π.
The problem we need to solve in order to achieve what we want:
- Keeping meaningful class name on development for ease of debugging -> πͺ we just follow best practices (BEM, SMACSS, OOCSS ...) or your own rule as long as it is consistency in your team.
- Outputing ugly/shortest class name on production for lightweight delivery and fast loading <- here this tough one ποΈββοΈ.
Outputing ugly/shortest class name: We need a tool which transforms class names, also keeps them synchronous other places when compiling our source code. I am currently using Webpack, luckily, one of webpack loaders named css-loader
has built-in function getLocalIdent
to support us to overcome this obstacle.
If you are familiar with webpack config, this is the simple version of increment name 0
, 1
, 2
... π£
That is the idea, you can write a better version than me, right? π
In order to remove unused selectors, we need to know which parts are used and which are not, therefore the first step is detecting unused selectors. How? There are two traditional ways to detect unused things on frontend code:
- Runtime: run your webpage, scan through HTML, CSS and js files to check which selectors are used in HTML/javascript. From used selectors, you/tools can exclude them and get unused selectors.
- Build time: via module bundler we can import CSS files and specify which selectors. Based on that information from module bundler, we can detect unused selectors (it sounds easy right? π€ͺ)
There are three popular ways to check unused CSS selectors:
- Command line: You run command line with the location of CSS/HTML/js files as input and output is used/unused CSS. There are a lot of tools but purgecss, uncss and purifycss are three top popular ones.
- Website: Quite similar to Command line but on a website. You enter your webpack's URL and it does its job π€ΉββοΈ.
For example: https://www.jitbit.com/unusedcss/, https://unused-css.com/, https://uncss-online.com/ ... - Chrome Code Coverage: The image below shows code coverage not only for CSS files but also for js files
How to open Code Coverage:
- Step 1: Open Chrome Dev Tools
- Step 2: Press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows, Linux), type
Coverage
- Step 3: Click record button
As I mention above. It sounds easy, right? Yes, it will be super easy if your module bundler supports named exports, otherwise, it can be challenging. I am currently using Webpack and unfortunately, webpack and its eco system (css-loader
) doesn't support named exports because moving from commonjs
to es6 named exports
is a breaking change, you can see why via this thread.
In order to climb a steep hill, there are two steps we need to deal with:
- Step 1: Support static import/export for css <- es6-css-loader (it is based on Webpack's offical loaders named style-loader and mini-css-extract-plugin.
- Step 2: Plugin to hook into webpack build process to show unused css selectors <- webpack-deadcode-plugin.
π I made the demo to demonstrate how to use two plugins above, here is source code.
When running the demo, an output looks like the image below
After applying those solutions above, our CSS bundle size at Bokio is reduced from 286 KB to 130 KB even we haven't applied removing unused selectors via build time yet. I will update our final result when we finish optimizing π€.