Mind the CSS: optimize CSS for faster page loads

One day I started looking into performance of the site I was working on. The main reason for doing that was because one of the OKRs I was working on is increasing conversion rate in the onboarding part of the site. I decided to focus on improving the load time of the site. There’re a lot of case studies online showing how load time affects conversion rates. They show that the first few seconds have the largest impact on conversion rates and that just a one second delay in loading time results in up to 7% loss in conversions. In terms of page sizes, less is always more.

Using Lighthouse tool from Chrome DevTools is the best starting point in the process of improving the performance of a site. Lighthouse will run audits against your site and generate a report with performance metrics and things you can do to improve the performance score.

Performance metrics from Lighthouse report

These are the audit results I got. Metrics that are shown in orange are the ones that can be improved. The report also shows tips on how to improve those metrics. Lighthouse identified CSS as my render blocking resource and one of the suggested improvements for the site was to reduce unused CSS. This improvement affects both the FCP (First Contentful Paint) and LCP (Largest Contentful Paint) metric. If you run the report on your site and also get a suggestion to reduce unused CSS, this article is for you. I’ll show the things I did on my site that helped improve the FCP and LCP metric.

Before I made any changes in the code, I wanted to see how long it takes for the stylesheet to load when someone enters the site for the first time ever. I wanted to know this since I assumed that the majority of people that visit the onboarding part of the site are visiting the site for the first time. This means that they don’t have any resources from the site cached in their browser. I tested the first page loads with different network speeds.

You can test page loads in the Chrome DevTools Network tab by selecting “Disable Cache” and choosing a network profile (No throttling, Fast 3G, Slow 3G or a custom profile that you can create).

Below is the result for No throttling profile. The stylesheet is 325 kB (67 kB compressed) and takes 218ms to load (and almost 5 seconds on slow 3G).

The bundle sizes of stylesheets are often overlooked when coding an app. A lot of devs consider CSS to be append-only, meaning that they will often add new styles in the stylesheet, but never delete old unused ones. That’s because deleting old styles is a hassle, takes a lot of time and you have to check the whole codebase to be sure that the style you deleted is not used somewhere. But CSS doesn’t have to be a big unmanageable mess! I’ll show a few simple things that I did that can help reduce the size of CSS stylesheets. The examples below are all from an Angular project, but you can reuse the same principles in any JS code.

Use CSS Variables

If you come across styles that look like the same duplicated styles with some minor changes, remember the DRY principle. Usually those styles can be a good candidate for using CSS variables. This is especially true for styles that are used for multiple themes. I knew the project supported multiple themes so I started with inspecting the code for them. I found the stylesheet that contained styles for different themes, there were around ten of them and they were all created using one mixin that takes four parameters: two fonts and two colors.

theme.scss

When this SCSS file was compiled to CSS, it generated around 500 lines of code for every theme. The styles for each theme were the same, with the only difference being the four parameters in the mixin. Instead of generating new styles for every theme, I created just one theme that will take CSS variables and then set their values through Javascript.

theme.scss

So instead of a value like “Inter UI”, we can use a CSS variable like var( — font-header) and then set that variable depending on which theme is selected. We first have to define values for each theme:

themes.const.ts

I then created a directive that subscribed to theme changes (a BehaviourSubject that returns the theme name every time the theme is changed), used a theme name to find theme values defined in themes.const.ts and set them into CSS variables.

theme.directive.ts

Since there were ten themes in the themes stylesheet, using CSS Variables reduced the file size to 1/10th of the original size.

Avoid CSS import

CSS @import is used to import one CSS file in another CSS file. I had one global stylesheet that used @import to load two other stylesheets. In the end all the styles were bundled in one big file. CSS is render-blocking, which means that the browser can’t show any content until this big bundle is downloaded. So instead of bundling everything in one file, a better approach is to separate stylesheets and load them in parallel. Since the project had three distinct style categories — theme styles, legacy styles and new styles, I decided to separate them into three bundles.

I removed @import ‘tailwind'; and @import ‘theme'; from styles.scss and defined three bundles: theme-styles, legacy-styles and new-styles. This can be configured in angular.json.

angular.json

Use PurgeCSS

PurgeCSS is a tool that analyzes your files, matches the CSS selectors that are used and removes unused ones. This is especially helpful if you’re using some CSS framework like Bootstrap and only use a small section of selectors from that framework. PurgeCSS can be integrated in the build process in the postbuild script that will be run after build script has completed. You can read more about integrating PurgeCSS in the build process here. I’ve reused the script from this article only changing the configuration which CSS bundles should be purged.

purgecss -css dist/legacy-styles*.css dist/theme-styles*.css — content dist/index.html dist/*.js -o dist/

I’ve configured PurgeCSS to clean legacy-styles and theme-styles bundles and I’ve skipped the new-styles bundle since that stylesheet is using Tailwind, and Tailwind generates CSS only for classes that are used, so purging this file would be unnecessary. Make sure to compare original and purged files to make sure everything is configured properly.

Here are the results after running PurgeCSS.

Below are the sizes and loading times of new bundles.

If we compare it to the initial bundle, we can see a reduction in size from 325 kB to 107 kB (or from 67 kB to 21 kB compressed) and reduction in load time from 218ms to 81ms (since these bundles are loaded in parallel, the total loading time is the time of the bundle that takes the longest to load). That is ~67% decrease in size and loading time.

I ran the Lighthouse report again, compared the results and saw improvements in almost every metric. (Lighthouse scores can change due to inherent web invariability, so make sure to run the report multiple times before drawing conclusions).

Saving a couple hundred milliseconds on initial load may not sound like a lot, but it was enough for the report to move the overall performance score for the page from average to fast. If there’s anything that I want you to remember from this article, it’s that improving web performance is not as hard as it sometimes seems and that a few simple tweaks can greatly affect the load time of your site. Besides CSS improvements, you can benefit from image compressions, lazy-loading your JS modules or replacing heavy dependencies. If you use the Lighthouse tool, you’ll know more about which part of your app needs attention. Do you have any performance tips or remarks? Leave them in the comments! :)

--

--