Reducing client JavaScript bundle size with Next.js

Elliot Heath

Elliot Heath

2022-06-29

Reducing client JavaScript bundle size with Next.js
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:
Including faker increases my bundle 4x 😩
Including faker increases my bundle 4x 😩
When I first included Faker, Angular wouldn't even let me build without modifying my bundle size budget.
🚩A red flag in retrospect 🚩
🚩A red flag in retrospect 🚩
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.

Poor vitals will cause your site to rank poorly among search engines

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:
  1. Forgo the feature at the cost of a less interesting app
  2. Keep the package at the cost of web vitals
  3. 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"

export async function getServerSideProps() {
  return {
    props: {
      name: faker.name.findName(),
    },
  }
}


export default function Component({ 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.
For example, in /api/name.tsx
import { faker } from "@faker-js/faker";
export default function handler(req, res) {
  res.status(200).json({ name: faker.name.findName() });
}
Then in our client component:
import { useState } from "react";

export default function Consume() {
  const [name, setName] = useState('Elliot Heath');
  async function getNewName() {
    console.log("getting new name");
    const json = await (await fetch("/api/name")).json();
    setName(json.name);
  }
  return (
    <>
      <div>name: {name}</div>
      <button onClick={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:
import { faker } from "@faker-js/faker"
export default function Mycomponent() {
  return (
    <div>{faker.name.findName()}</div>
  );
}
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:
2.4 MB just to generate 'David Koss'
2.4 MB just to generate 'David Koss'
Compare this to when we use getServerSideProps():
When faker isn't sent over the wire, the component is only 3.9 kB
When faker isn't sent over the wire, the component is only 3.9 kB