Differential Serving — Serve legacy code to old browsers and ES6 code to modern browsers
In my organisation, my team has been working on the performance improvement of the website for quite some time now. And a few days back we decided to ship ES6 code to our users as 91% of our users were on browsers supporting ES6. Even Google Lighthouse and Google PageSpeed Insights started showing suggestions to stop shipping legacy code. So let me tell you how you can also enable differential serving on your website as well and finally share my results and insights.
First of all, what the heck is even Differential Serving?
Differential serving is compiling and serving separate bundles for different browsers. Usually, you create one bundle of ES5 code for old browsers and another bundle of ES6+ features for modern browsers. This is done at the build time only, so the user won't know about any of this.
Now, why even bother for Differential Serving?
First of all, let's see the support of ES6(ECMA 2015) features by the browsers worldwide. You can also view these results at Can I use.
You can clearly see that 93.76% of all the users worldwide are on those browsers which completely support ES6 features. This number is really huge and still, most of the websites are shipping old legacy code to their users by transpiling the ES6 code to ES5 code, which is like going backwards.
Okay, I know what and why now tell me how to achieve Differential Serving?
To achieve this, we usually make changes in babel config, webpack config, some server-side changes if you do server-side rendering and that's it. Since we need to ship different types of code to different types of browsers, we need to have 2 babel configs and 2 webpack configs.
1. Babel Config
Usually, everyone would have a single babel config file. But for this, we need to have 2 babel configs:
Legacy babel config:
Modern babel config:
To generate ES6 code, we used esmodules: true
in our modern.babel.config
file. Since most of the code was same, we made plugins common for both config and just imported them.
Since babel-loader internally uses caniuse, you can check the browser versions that babel uses for
esmodules
, atnode_modules/@babel/preset-env/data/build-in-modules.json
.
2. Webpack Config
Just like 2 babel configs, we will need 2 webpack configs, legacy and modern. I have removed unnecessary code from the gist to keep it concise and will only list down the key differences in both the configs.
Legacy webpack config
Here we have different output build folder named legacy-build
. We also append .es5.js
to all the files and chunks to differentiate in dev-tools. We also use legacy babel config here to transpile ES5 code. One more thing is since we use loadable components, we need two legacy stats file for it.
Read more about code splitting using loadable components here.
Modern webpack config
Here we have different output build folder named modern-build
. We also use modern babel config here to serve untraspiled code. We generate modern loadable stats file, different from previous.
These configurations might differ according to your code base and plugins used.
3. Server-side changes
To server the correct type of bundle to the browser, there is one popular way of achieving this by using ES6 modules. You can read about this approach in detail here.
But we didn't use this approach since we do server-side rendering (SSR), so we need to take the decision on the server-side but the former takes the decision on the client-side.
Now to figure out the correct bundle type, we wrote some custom logic based on the browser name and the browser version of the user. Babel also does the same thing using browserlist. But we could not use it on our server since our backend is in C# instead of node.
Here is our custom logic to detect modern browser:
Now you will wonder how do we get browser name and version? So we were already using a paid third-party library for this. But you can use any free third party library. All of them work based on the User-Agent of the request and process it.
One more thing to note is that since we used loadable components, we had 2 loadable stats file. So we need to conditionally use one of them for SSR based on if the user’s browser is modern or not using above logic.
Results
Right off the bat, you can see the difference in total bundle size
and javascript size on page load
. We also saw improvements in script evaluation time
and script parsing and compilation time
. All the below metrics were measured using Google Lighthouse.
1. Reduction in total bundle size
Our total bundle size(gzip) decreased from 2.03 MB
to 1.85 MB
, which roughly translates to an 8.9% reduction in total bundle size. You can say that it's not too much but as your project size increases, you get diminishing returns. The main reason being the polyfills. Polyfills contribute more to the than code-size due to transpiling.
2. Reduction in Javascript size on page load
Earlier 432 KB
of javascript was loaded on page load but now it is 392 KB
. Here we reduce 40KB(gzip) of javascript on page load which is a lot 🙀.
3. Reduction in script evaluation time
Our script evaluation time went from 3097.4 ms
to 2769.8 ms
. Here we save around 327.6 ms in total.
4. Reduction in script parsing and compilation
Script parsing and compilation went from 264.8 ms
to 208.4 ms
, which is nearly 56.4 ms reduction. This is not huge because these are comparatively smaller than third-party scripts.
Some Important things to note:
- If you are using TerserWebpackPlugin to minify your bundle, then you need to pass
ecma:6
andsafari10: true
in your terserOptions. - If you are caching your server-side HTML, then there will be two sets of caches instead of one because of 2 different webpack bundles.
- Since jest uses your project’s babel config file, so you need to tell jest your corresponding babel config by doing:
"transform": {
"\\.js$": [
"babel-jest",
{"configFile": "./babel/babel.modern.config.js"}
]
},
Conclusion
Referencing above results, the gains that we are getting and the amount of effort we put in, I can definitely say that it's worth giving a shot and this should be an industry-standard by now. Plus there is no maintainability as we just need to tweak our webpack and babel configs.
Good articles on Differential Serving
- https://philipwalton.com/articles/deploying-es2015-code-in-production-today/
- https://twitter.com/CharlieCroom/status/1291478104016289799
- https://dev.to/thejohnstew/differential-serving-3dkf
- https://calendar.perfplanet.com/2018/doing-differential-serving-in-2019/
For any further queries or questions, comment down below or reach out to me on my email : amitsingh5198@gmail.com