A Week in my Relationship with Webpack and React

A Valentine’s Day Special — Memoirs of a Lovers’ Quarrel and Triumph 💕

Adam Henson
8 min readFeb 14, 2019
Taco and Chico near Penn Station next to a tear sculpture by Richard Hudson

Day 4, the Webpack Upgrade

After all we’ve been through this week… I take a step back and reflect on the root of our recent rough patch. Was it the late night “code splitting”? Was it our choice to adopt a “universal” architecture? Maybe we should have pumped the breaks before diving right into React Toolbox and cssnext. After 4 days and little sleep, the battle continues. It all seems so simple as explained in your official documentation… take splitChunks.chunks for example.

This indicates which chunks will be selected for optimization. When a string is provided, valid values are all, async, and initial. Providing all can be particularly powerful, because it means that chunks can be shared even between async and non-async chunks.

In my head — I repeat those words to make sense of them. With you everything is so abstract!

Okay, okay, maybe the answer is really behind the definition of a cacheGroup. That seems to be the place we’re essentially defining chunks nowadays… right? I turn to the documentation again.

Cache groups can inherit and/or override any options from splitChunks.*; but test, priorityand reuseExistingChunk can only be configured on cache group level. To disable any of the default cache groups, set them to false.

You really lost me this time. Maybe this isn’t really meant to be… should we give up?

Identifying the Problem

Before making major changes to a software application, it’s important to define and weigh objectives. What exactly is the problem we’re trying to solve?

In looking at web page performance, Google Lighthouse is a tool that creates audits based on specific metrics. Time to Interactive is the highest weighted metric in the overall performance score (as of Lighthouse v3). In the documentation, Google makes recommendations to improve this metric.

To improve your TTI score, defer or remove unnecessary JavaScript work that occurs during page load. See Optimize JavaScript Bootup and Reduce JavaScript Payloads with Tree Shaking, and Reduce JavaScript Payloads with Code Splitting.

Identifying and Trying Possible Solutions

In my case, upgrading from Webpack 1 to 4 was long overdue. I wanted to take advantage of new features in order to improve performance in my web app. Features like support for dynamic imports, tree shaking, and a more granular configuration for code splitting would potentially help in this effort, especially since Google explicitly recommends to “Reduce JavaScript Payloads with Code Splitting”.

According to Webpack’s docs, there are 3 general approaches to code splitting:

Entry Points: Manually split code using entry configuration.

Prevent Duplication: Use the SplitChunksPlugin to dedupe and split chunks.

Dynamic Imports: Split code via inline function calls within modules.

Fair enough! Now what? Interpreting this and devising a strategy is the challenging part. I (probably unfortunately) learn a lot of things by trial and error. The 4 days it took for me to wrap my brain around code splitting was spent somewhat blindly choosing directions and reverting… mainly trying different combinations of split chunk configuration. Webpack is well documented, however it can be quite difficult navigating through such a high volume of concepts and combinations of concepts that are typically reliant on each other.

I decided the heaviest part of my application, in terms of bundle size, was based on logged in state. When a user is logged in — pages show data visualizations utilizing some very heavy chart libraries. I thought perhaps I could split and consume chunks based on logged in state. I hacked together an odd combination of the first two code splitting approaches (listed above). Let’s just say — it wasn’t successful. For something like this, I’d need to use the entry point approach like Webpack’s multi page application example. This would be impossible based on the way my app was constructed.

I ended up backtracking and deciding that a simple configuration would be best for now:

By examining the visualization from Webpack Bundle Analyzer, we have a good insight into the current state of code splitting in my app. The vendor bundle which includes code from node_modules could be better — 277.44kb gzipped 🤔.

Day 5, Dynamic Imports and React Upgrade

It’s been a rough patch in my relationship with Webpack indeed. I was up until 3am last night… for what?! I ended up reverting the days of different approaches. In the thick of my hack — I re-organized logged-in only components so I could attempt to split code using cacheGroups and filtering by regular expression matches of the context directory. This was the wrong direction. But then I realized I had only devised one solution. Maybe the reason why my solution failed was because I didn’t really pinpoint the actual issue 💡!

Identifying a Real Cause

Sometimes identifying the actual root of the problem takes time and re-evaluation. When digging deeper, I determined that there were actually only a couple libraries causing the bloat we saw in the vendor bundle. I narrowed it down to two and they were both being imported by a single component. The component is named ApiVisualizationChart — a chart for showing data from an API. Why not just “ApiChart”? Forget it… it’s complicated (naming isn’t my thing). What if I could split that code from the vendor bundle and only load it when needed? Is this possible, even in a “universal” app? You bet it is!

React 16

It’s come time for me to bite the bullet. In order to utilize the newest libraries and versions of libraries in React, I need to face my fear. Upgrading major libraries has been a painful experience for me in the past, but usually in large corporate engineering teams. I love the new features in React 16, but I’ve been putting off the upgrade for a while. I have to admit, it was the most painless part of my week. With this simple step, the Lighthouse performance score magically increased by 2 points for my app. Nice one React 💖!

Dynamic Imports and Loadable Components ⭐

Another approach to code splitting, and the one that worked best for me, is to use the import() syntax that conforms to the ECMAScript proposal for dynamic imports, supported by Webpack 4. For this to work, we don’t need to touch our Webpack config (shown in the Gist above). Assume the following for the ApiVisualizationChart example component I mentioned before:

├── ApiVisualizationChart.js
├── ...(other components)

That may not look like a lot of code above, but what it actually adds to our bundles is pretty mind blowing. Even more mind blowing is the way we can use dynamic imports and Loadable Components to address it. I should mention — I didn’t entertain React.Lazy as it doesn’t support Server Side Rendering (SSR), at the time of this writing.

Note:

React.lazy and Suspense is not yet available for server-side rendering. If you want to do code-splitting in a server rendered app, we recommend Loadable Components. It has a nice guide for bundle splitting with server-side rendering.

Fair enough… carry on React. Loadable Components it is. I prefer using core features when possible, so if you don’t need SSR support — I’d recommend React.Lazy, but you decide! Loadable Components provides their comparison.

In my specific case I only care about loading the chart component client side, so I decided to take a deeper dive in SSR at another time… but at least now I know I’m setup to support it. Loadable Components provides documentation about SSR usage.

To accomplish this, I modified the structure of my component. The actual component code and imports of it don’t change. I now have:

├── ApiVisualizationChart
│ ├── index.js
│ ├── ApiVisualizationChart.js
├── ...(other components)

And I create a new bundle dynamically with the new file below. That’s right — it’s that simple!

You may be wondering what the funny looking comment is about. These are magic comments for Webpack. For this component I disable SSR as documented.

And now, checkout the state of our bundles!

We reduced our vendor bundle by 119kb gzipped 💥! ApiVisualizationChart imports the chart.js module which depends on moment which is quite a large library. And now we have two new bundles, with a name derived from webpackChunkName defined in our magic comment — ApiVisualizationChart.[hash].js and vendors~ApiVisualizationChart.[hash].js. The awesome part about is the two dynamically imported JS files are requested and interpreted asynchronously, client side, on demand! By using Webpack 4, dynamic imports and Loadable Components, this is all built in.

Every page of my app will now load vendor~main.js and main.js during page load via script tags managed in the server side render.

Like magic, if a page is supposed to render ApiVisualizationChart component — it does so “asynchronously” after load of our dynamically created bundles (after initial page load).

Day 7, Triumph 💕

Chico and Mama… Chico Just Doing Chico

No one ever said it would be easy, but this relationship of mine with Webpack and React always proves to be positive in the end. By implementing code splitting — I was able to improve the Lighthouse performance score of my app’s home page by 6 points. The cumulative effort of all the changes outlined in this post improved performance score by 12 points (an average over 5 runs for each comparison).

Conclusion

As an engineer working in web development for over a decade, I come from an era of growing JavaScript fatigue. Most of the publicly known engineers I look up to are a decade younger than me, as a bi-product of the newest trends becoming the most widely adopted. I remember when jQuery vs MooTools was a thing.

My relationship with Webpack and React has had its ups and downs over the years, but ultimately it’s been the most thought provoking, well documented, and longest living stack I’ve worked with. The declarative paradigm can be quite valuable, but can also yield a web of obscure functionality and concepts when definitions and documentation are abstract. I’ve come up with some basic rules that help me cope in this rapidly evolving ecosystem.

  • “Less is more”. This one has stood strong in the test of time.
  • Always question the value in choosing to adopt a methodology, library, or other major change. Explore solutions with research, trial and error.
  • Acknowledge what you don’t know, and identify it as opportunities for future learning. It’s important to maintain humility and accept that you don’t know everything. Things worth learning, relevant to you will have their time and place. It was humbling to read such admissions from such a well known, admired engineer like @dan_abramov in Things I Don’t Know as of 2018.
  • Only take on what you can. We’re human — it’s important to avoid biting off more than we can chew. I try not to let the things I don’t know worry me too much and learn things one day at a time.

--

--