Speeding Up Tesla.com - Part 2: Main CSS Splitting
Tags: webperf, css, javascript, webpack
Published: 2020-06-09 07:10:29

<p>In <a href="https://dev.to/platformos/speeding-up-tesla-com-part-1-images-and-js-minification-1b22" target="_blank">part 1 of this series</a> I did three things to make <a href="http://tesla.com">Tesla.com</a> a little bit lighter and faster:</p>
<ol><li>Compressed image and used JPEG format - Saved 6.7 MB + improved perceived performance by using progressive JPEG</li><li>Removed unused font - it made the page lighter by 150 KB, and because it was inside CSS, it is no longer blocking rendering</li><li>Minified the boomerang.js library - 120 KB → 40 KB</li></ol>
<p>In part 2, I will dive into the main CSS file and see what I can do to make it lighter and load more efficiently.</p>
<h2>Why lazy loading CSS?</h2>
<p>The size of assets is one part of the story — It's important, but it's not the only important factor. Just as important as the size of the assets a user has to download is how you serve them, when you serve them and when you execute JavaScript.</p>
<p>CSS is a render-blocking resource. This means that until the CSS discovered during the HTML parse phase is downloaded, parsed, and applied to the HTML page, nothing gets rendered. That's why it is very important to:</p>
<ul><li>Keep CSS as small as possible</li><li>Inline critical styles, when appropriate</li><li>Lazy load, when appropriate</li></ul>
<p><a href="http://tesla.com">Tesla.com</a> has a lot of CSS, the biggest one is 2.5 MB. That's a lot. A quick investigation showed that it is full of inlined assets, some of them encoded in base64 (base64 in itself has around 15% overhead). I will not go through the trouble of decoding all those resources into their proper SVG formats, but I can split this CSS into logical parts that can be loaded asynchronously.</p>
<h2>Solutions - summary</h2>
<ul><li>Remove duplication (667 KB)
<ul><li>Removed all base64 icons that were included twice</li></ul>
</li><li>Extract country flags (640 KB)
<ul><li>All country flags used in the country selector in the menu, unfortunately, base64 encoded again</li></ul>
</li><li>Extract locale icons to separate CSS (~572 KB, estimated)
<ul><li>Apple and Google Store localized icons</li><li>There are only 2 localized icons, so you could argue that it is not worth the hassle to create a separate CSS file per locale. You would be right in this case. I did it to show the mechanism of how it can be fixed as a principle, even in projects with a lot of assets depending on locales. This solution works for JS, images, CSS, and everything in-between.</li></ul>
</li><li>Extract icons to separate CSS (305 KB)
<ul><li>Icons that are universal across the page. Most of them are not used on the homepage anyway.</li></ul>
</li><li>Extract fonts (Gotham) to separate CSS (380 KB)
<ul><li>To achieve a good result follow my <a href="https://dev.to/platformos/optimizing-font-files-for-modern-web-3jm8">guide on optimizing font files for the modern web</a> - I don't want to spend too much time on this because there is already a lot to do in this one file.</li></ul></li></ul><br>
<h3>Size result</h3>
<p>After all the removals and extractions, the main CSS is 366 KB. This is over 2.1 MB (~90%) less and this is the size of the CSS that is blocking the rendering of the page when it's downloaded. The rest of it is asynchronously loaded later on. Perceived performance should improve a lot just by doing that.</p>
<p>One thing that I noticed across this file is that it uses an extreme number of media queries. At this point, I would just separate this CSS into breakpoint-specific CSS and not pack it into one.</p>
<h2>Webpack</h2>
<p>It is very easy to load JS and CSS on demand using webpack.</p>
<p>This is what the main JavaScript file looks like at the end of the process:</p>

```js
import(/* webpackChunkName: "icons" */ './icons');
import(/* webpackChunkName: "flags" */ './flags');
import(/* webpackChunkName: "gotham" */ './gotham');

const locale = i18.locale;

if (locale !== 'en_US') {
    import(`./locale/${locale}`);
}
```

<h3>Remove duplication</h3>
<p>While working on extracting icons, I noticed that apart from being encoded, they are included twice. Once in a form of base64 and once as an SVG source directly. This is a huge waste, so let's remove the base64 ones since they were overridden later on, so not used.</p>
<h3>Extract country flags and icons to separate CSS (one per locale)</h3>
<p>In the page source I found these couple of lines:</p>

```js
var i18n = {
    language: 'en',
    region: 'US',
    locale: 'en_US',
  };
```

<p>This means that the default locale for the website is en_US, and it changes when the user changes the language. The browser, during runtime, can determine which locale should be loaded, so we can easily load only the one we need, instead of loading all 44 of them.</p>
<p>And then we need JS files for locales, because in webpack 4 (in version 5 that will change) CSS cannot be an entry point.</p>

```js
import './de_de.css';

console.log('Loaded de_de.css');
```

<p>These couple lines of code will generate JS chunks for the locales and CSS chunks for all the CSS locales imported inside of those chunks. Webpack will load only the appropriate JS chunk when (<code>locale/${locale}</code>) and this chunk will load only the appropriate CSS.</p>
<p>After that, I extracted the German locale to <code>de_de.css</code> as a proof of concept and left only en_US inside of the main CSS file.</p>
<h2>Results</h2>
<p>Devtools show that there is a whole lot less CSS than it used to be. And it is loaded more reasonably. Some CSS is still blocking, loaded in head, some of it is loaded asynchronously.</p>
<p><img src="https://uploads.prod01.oregon.platform-os.com/instances/1374/assets/images/blog/tesla-part-2/css-loading.png" alt="CSS loading asynchronously"></p>
<p>The German version also loads de_de.css:</p>
<p><img src="https://uploads.prod01.oregon.platform-os.com/instances/1374/assets/images/blog/tesla-part-2/de_de.png" alt="Localized styles loading"></p>
<p>At the end of the journey we got something green in Lighthouse:</p>
<p><img src="https://uploads.prod01.oregon.platform-os.com/instances/1374/assets/images/blog/tesla-part-2/lighthouse-after.png" alt="Lighthouse test result - after"></p>
<p>What is even more important than Lighthouse score, is when the user sees and can interact with the page, which is clearly better and is visible on the webpagetest.org test: <a href="https://webpagetest.org/result/200526_0G_cb466cf80f135f4e66c24dab58338cd2/">https://webpagetest.org/result/200526_0G_cb466cf80f135f4e66c24dab58338cd2/</a></p>

![Webpagetest result - after](https://uploads.staging.oregon.platform-os.com/instances/335/assets/images/webpagetest-after.jpg)

<strong>User can see and use the site after 4 seconds. Before it was 17.</strong>

<h2>Conclusion</h2>
<p>Knowing and using the correct techniques for certain situations makes or breaks the page. In 2020 we have many good tools to implement those techniques. With webpack and other asset bundlers, it became easy to handle all those heavy webpages with grace and prevent user experience degradation even if you need 200 KB+ of JavaScript to show a blog post or your marketing cannot live without Google Tag Manager which is a topic for a whole another article.</p>
<p>This article's conclusion is that everybody makes mistakes. We live in an imperfect world, and performance is more of a process than a one time job. I'm pretty sure all the issues that arose in Tesla's codebase are the results of hundreds of decisions and a lot of pressure for deadlines. But an iterative approach makes sure that everything can be changed. Fortunately, there is much more room for improvement.</p>
<h2>Source code</h2>
<p>You can see the results and source code here:</p>
<p>Part 1: <a href="https://github.com/pavelloz/tesla.com">https://github.com/pavelloz/tesla.com</a> - <a href="https://tesla.prod01.oregon.platform-os.com/part-1">https://tesla.prod01.oregon.platform-os.com/part-1</a></p>
<p>Part 2: <a href="https://github.com/pavelloz/tesla.com/tree/part-2">https://github.com/pavelloz/tesla.com/tree/part-2</a> - <a href="https://tesla.prod01.oregon.platform-os.com/part-2/">https://tesla.prod01.oregon.platform-os.com/part-2/</a> and <a href="https://tesla.prod01.oregon.platform-os.com/part-2-de/">https://tesla.prod01.oregon.platform-os.com/part-2-de/</a></p>