Code splitting and chunking Frontend apps with Webpack
We are living in the era where we have phones that are faster than computers of previous ages, we have 4G connections that are faster than the broadband of the old. But we still haven't received salvation from the ugly user experience of slow websites, from the high page load time numbers, that are blaring at the developers face that your site is not fast enough. There is a never-ending desire by product managers to send as many features as possible to the end user, and for us as developers, to minimize the amount of payload that gets transferred in each transaction, escaping the hell bestowed upon by high load times.
Traditionally, designs of frontend apps have leaned towards separating the code based on context and domain. A first level separation of HTML, JS and CSS: sending the individual files separately, and a second level separation based on the domain: sending JS files from different domains separately. This approach made sense when the JS payloads were small, for example, a static website, or when we lacked adequate tooling to create a difference.
But now in the presence of tools like webpack and parcel, colossal sized JS files are essentially squandering the time of:
- The end user, by sending him features he may never use
- The browser by making it parse unused javascript
is there a way out?
Short answer, Yes.
Code Splitting and Chunking
There are various paradigms that have emerged as small forays into increased performance. Some have tackled code splitting based on functionality, some have tackled code splitting based on components. The common goal that both approaches share, is sending a critical amount of code that the user will potentially use. We, on the other hand, had a single monolith of Javascript bundle code whose development size is substantial huge i.e 7.7MiB(neither minified nor gzipped).
so the best thing given the circumstances was to approach code splitting based on Routes.
Why would you want to split based on routes?
Initially the file where routes were defined, looked like:
Here, for all the components, we have used 'require'. This signifies that webpack will push them inside the primary bundle that will be served to the client. But then, a user cannot be at two pages at the same time, so at each route, other routes become a part of the 'un-necessary' code.
Suppose your application has two routes:
1. '/' ← The homepage, where there will be a Homepage container executing the code
2. '/hotels-in-:location/:hotel.html' ← The HotelPage ( our product display page ), where there will be a Hotel container executing the code.
Now there is supposed to be ( and always is ) some code that is common between '/' and ‘/hotels-in-:location/:hotel.html’. Further, there is some base ‘framework code’ that is needed to execute both for both these routes.
So what can we do?
One approach is:
1. bundle.js // The JS file containing the common as well as bootstrap code and node modules for the app.
2. homePageChunk.js // Chunk containing exclusively home page code, needed by the user only when he lands on the homepage
3. HotelPageChunk.js // Chunk containing exclusively hotel page code, needed by the user only when he lands on the hotel page.
For the first nose-dive into performance, this uncluttered approach seemed the best way to head out.
While defining routes in react router, either version 3 or version 4, We set up the getComponents Key of the routes to a callback. This callback is executed once when the route is actually visited for the first time. The second argument to the callback (cb) is the component that will be loaded henceforth whenever this route is visited.
The callback to require.ensure supplies an asynchronous require function that dynamically requires the module, and calls the callback (cb) on that module, to make it the default component for this route. The second argument of require.ensure is the chunk name that will be given to the chunk, “HomePage” in this case.
In this case the size of bundle gets reduced from 7.7 to 2.73 MB. So suppose a user lands on the homepage, the JS payload now being sent to the user would be:
bundle.js + HomePage.chunk.js => 2.73MB + 281KB = 3 MB
We achieved a reduction of 61% in the amount of data we send the user on the homepage.
For our HotelPage, it was
bundle.js + HotelPage.chunk.js => 2.73MB + 1.61MB = 4.34 MB
That accounts for a reduction of 43%.
This is our Product display page, stuffed with features that may or may not be directly visible to the user, perhaps hidden behind a click or other actions.
So 43% looked good. But can we do more?
Component-based splitting
Now, there are many libraries like react-loadable that get the same thing done. But we were experimenting to find something generic and which gives us more control over when we and how we implement components, without the overhead of other libraries. We realized that the same concepts of Route based splitting can be used to asynchronously require components that are not part of the first render.
Components that hide behind a user interaction, Components that required libraries like HammerJS that no other component in our project was using.
The logic is very similar to how we went about doing things in our routes. Just that in this case, we can hide this component behind a user interaction. Rendering it only when the user clicks on the 'CLICK TO RENDER' button.
Ultimately the size of HotelPage.chunk.js got reduced from 1.61 to 938KB. A total deduction of 43% in the size of the hotelpage chunk.
The overall deduction was around 50% for hotelpage.
What else can we do?
Webpack Splitchunks plugin:
Webpack offers an in built split chunks plugin, that we are experimenting with right now. Often dubbed as "the mysterious splitchunks plugin"
https://medium.com/dailyjs/webpack-4-splitchunks-plugin-d9fbbe091fd0
It is a fairly less understood but very powerful and highly customizable plugin. It aggressively splits these chunks further based on whether they are initial chunks, async chunks or both. We will be rolling out the next iteration of this blog where we feature on things like the pros and cons of using dynamic import, webpack 4 splits, and some ingenuine things that we can do with react for delayed loading of chunks for serverside rendered applications.
This blog has been made possible with the joint effort of Prashant Singh(Tech), Charanjeet Kaur and other team members at Fabhotels. This is a part of the ongoing series where we have started jotting down things that we have done in the past one year to make the website performant. Big thanks to Vivek Rai, Ankit, Nasir, Surbhi, Ankur and Javed for their contribution. Our goal is 1 second on Indian 4G, and we will slowly be rolling out blogs about cutting edge stuff that we do.