Reducing client JavaScript bundle size with Next.js
Elliot Heath
2022-06-29
I'm building a silly browser game and my JavaScript bundle quadrupled in size with the inclusion of a single package
I like to insert placeholder data into input forms to inspire my users, so I leverage @faker-js/faker to generate names, nouns, adjectives, animals, etc. you name it. Now, I have nothing but good things to say about Faker, but look at this bullshit:
When I first included Faker, Angular wouldn't even let me build without modifying my bundle size budget.
I'm not knocking Faker or trying to single it out. In fact, I love Faker and I want to use it in my game. But does its inclusion make my user experience four times better? I would argue not like this, because
Large bundle sizes negatively impact a page's web vitals
A user with a lousy internet connection might think your website is down when really it just needs more time.
A user might become irritated when your page begins to render, but he or she cannot interact while JavaScript continues to download.
And lastly, a user will become infuriated when the layout of your page changes while they are interacting with it.
These three experiences are commonplace to an app that performs badly on the core metrics: loading, interactivity, and visual stability.
Which of course, makes it harder for your site to gain organic traffic. In my case, sure, fake placeholder data might be a nice touch for the users who actually stick around to see the page load. But what about all the users who don't get to see any feature of my game because they never even find the app?
So what should be done? If you don't know any better, you might think compromises are your only choices:
Forgo the feature at the cost of a less interesting app
Keep the package at the cost of web vitals
Implement the feature yourself at the cost of your vitals 🤬
Next.js: no compromises
Next.js can keep your initial bundle size small and retain the use of an otherwise weighty JavaScript package.
Next.js provides many solutions but broadly there are two strategies: keep the package entirely on the server or, if you must, send it to the client responsiblyâ„¢
Keep the package entirely on the server: SSR
If the utilities from the package are only being called once, consider a Server Side Render (SSR) approach.
At the time of the client's first request for a page, Next.Js permits developers to run some code using getServerSideProps(). The result of this function can be passed immediately into the HTML response that is sent to the client.
So in my case, should I need one random name:
import{ faker }from"@faker-js/faker"exportasyncfunctiongetServerSideProps(){return{ props:{ name: faker.name.findName(),},}}exportdefaultfunctionComponent({ name }){return(<div>Hello {name}</div>);}
Client initiated requests for data
Perhaps the client will need additional data after load or perhaps you don't wish to use SSR. For this, wrap the Faker code in an API endpoint for client consumption.
import{ useState }from"react";exportdefaultfunctionConsume(){const[name, setName]=useState('Elliot Heath');asyncfunctiongetNewName(){console.log("getting new name");const json =await(awaitfetch("/api/name")).json();setName(json.name);}return(<><div>name: {name}</div><buttononClick={getNewName}>Get random name</button></>);}
Depending on the data coming back from the API, the size of these requests are less than a kilobyte. Think of the bandwidth savings if you are using a metered hosting provider! In terms of bandwidth, a users would have to average above 5000 API requests—before refreshing or navigating away—to exceed the size of loading Faker one time. That seems like an extreme outlier case not representative of the average user. And even in such a scenario with excessive, you probably still wouldn't want to weigh down the initial page load with Faker anyways.
The wrong way
Here is how you don't want to access the Faker package:
Notice that the call to faker exists inside the component sent to client. Meaning, this will send the package over the wire, which is what we are trying to avoid. You can verify this by looking at the dev tools network or source tab: