25.33% Reduction in First Load JS with NextJS Dynamic Imports
A play by play in successfully reducing first load JS bundle size by 25% with code splitting via dynamic imports, and getting all NextJS pages into the green zone.
NextJS is nice for so many reasons, and you'd be doing yourself a disservice by not using it in 2021 for web development. But that's besides the point of this post.
My goal with this post is to document my own personal success story and exploratory efforts with NextJS and code splitting for improving page load time. Hopefully this will be both high-level enough to communicate benefits to say, a hands-off engineering manager or a backend developer, and in-depth enough to point a frontend developer in the right direction if they were to explore this area themselves.
Here's a quote from the NextJS docs.
next buildcreates an optimized production build of your application. The output displays information about each route. The first load is colored green, yellow, or red. Aim for green for performant applications.
When you use Next CLI, this little bit of feedback on its own is immensely valueable for monitoring your own bundle size. Every time you build your application, the NextJS CLI outputs your First Load JS for each of your pages. From this, you can get a rough estimate of how well you're doing in terms of page speed.
I'm unable to replicate the CLI output colors exactly as they would appear, so I'm substituting them with green/yellow squares here: 🟩 / 🟨
Here's my initial
next build output.
The one highlighted line is what triggered onset of this article.
I needed a more granular breakdown of my bundle size so I used
@next/bundle-analyzer which also provided
yarn add @next/bundle-analyzer
ANALYZE=true next build
The browser should automatically open the bundle analyzer:
From a quick glance at the visualizer, I made an educated guess that the
which is reused by all my pages, would be the spot for potential improvements.
Looking at the tree structure of the Layout component,
The first thing I noticed was 3rd party components that I was pulling in from @material-ui into
Header component. I made yet another guess that replacing those with homemade components could shave down on some
Without diving into specifics, this particular component refactor happened to be a light lift — it took about 5 minutes — but it didn't result in significant improvements to first load JS.
I tried dynamically importing the entire
Header component to see what that could change:
I was under the impression that this would dynamically render the header, which would increase culmulative layout shift (cls), which would hurt Chrome's lighthouse accessibility scores.
I checked lighthouse before and after and didn't notice any regressions in CLS, so this looked ok.
You might be thinking, "Woah, thats magic!", and then questioning, "Wait, where do those extra kB's go?"... If you're familiar with the Law of Conservation of Mass, the bytes-equivalent applies here.
"Bytes are neither created nor destroyed. They're simply redistributed."
css/9966a83c0eaa2c20cbb1.css (3.11 kB) chunks/8eb64b0ea41808d73389c1b6dbe939bfae9ed001.fb132b.js (10 kB) chunks/ed20a0fe1cf8573e8666bc854e61e3f8486924a2.c14d5a.js (4.36 kB)
The math adds up too. The 17 kB removed from First Load JS gets created as 3 new chunks, totalling 17.47 kB.
Moving on, I applied the same dynamic import to the footer.
My dynamic pages —
/[year]/[month]/[day]/[slug] — were no longer the largest pages.
At this point, all my pages were already green, but I wanted to test if this final dynamic import would have any benefits.
All that appears to be happening now is some JS is being redistributed.
/: 108 kB → 107 kB
- First Load JS shared by all: 80.5 kB → 81.4 kB
For demonstration purposes, I'm mashing together the outputs of the
next build command,
from before and after all my exploratory work.
Assuming my before-and-after comparisons makes sense, here are my conclusions.
/[year]/[month]/[day]/[slug], which is also a dynamic route for N-number of statically generated pages — decreased by 25.33%, from 133 kB to 99.3 kB
This brought all the pages into NextJS's green zone. 🎉
And for fun, here's a shot of the in-browser lighthouse score. (Although https://web.dev/measure/ is a more accurate assessment.)
So, why is this good? Faster page load times means better lighthouse scores which means better search engine ranking which means more traffic which means greater potential for new customer acquisition (if you're a business) which means more money! (Just assumptions here.)
🎉 💯 💯 💯 💯 🎉
No. Just because you can doesn't mean you should.
Using too many dynamic imports actually reduces performance. Instead of a single request, you now have the overhead of several requests, for content the user wanted to see anyway.
Dynamic imports are best used for things like modals — you don't know if the user is going to open your modal, so you dynamic import it, and it doesn't get downloaded until the user clicks.