Building for Production
Throughout this course, we've been talking about the difference between “development mode” and “production mode”.
In “development mode”, React and Next are both optimized for the developer experience. We get things like:
- Better error messages.
- Improved integration with the React developer tools
- React Strict Mode, helping us catch edge-case issues by doing things like running our effects twice.
By contrast, “production mode” is optimized for the user experience. Essentially, it's a slimmed-down mode focused on performance. The JS bundles become way smaller, and React's render performance becomes much better.
Occasionally, it's worth checking to see how our application performs in “production mode”. This will give you a much more accurate picture of what it's like to use your application.
In this lesson, we're going to talk about how to build our applications for production, and how to run the production build locally.
Video Summary
This video shows how to build our sample-next-app
from the previous lesson for production.
First, interrupt your dev server. Funky things will happen if you try to run a build while the dev server is still running.
Then, run this command:
$ npm run build
This will generate all of the files needed to serve our application in production. The JSX will be compiled into JS, individual files will be bundled into logical chunks, and the routes themselves will be created.
You should see some output like this:
We can ignore the second table; it refers to the legacy “pages” router, which is currently only being used for the 404 page (I imagine this route will be moved over to the newer App Router soon).
Each row is a different route. We see the home route (/
) is a 5kb file with 82kb of required JS. The /about
route is smaller, since there isn't much content, and it requires a bit less JS (since it isn't using next/image
or next/font
).
These files get generated within the .next
directory. This directory is essentially Next's workspace. It generates files as-needed, both for the dev server and the production build. This is why we can't have a dev server running while we do a build: it winds up overwriting important files!
If we spelunk in this directory, we can find the HTML files associated with both of our routes, but we can't exactly open them. We need to run a Next server which will serve the files and do any dynamic work required.
We can do that with the following terminal command:
$ npm run start
This will launch a new server, similar to npm run dev
. It even defaults to the same port!
Unlike the dev server, however, this is a production server. It uses the production version of React and Next. This means it's much more representative, in terms of bundle sizes and runtime performance.
It also doesn't have a hot reloader; if we make any changes to our code, we need to interrupt the server, re-run the build, and re-run the server.
And so, it's too high-friction to use for our general workflow, but knowing how to build and run a local production server can be really useful when doing performance testing, or trying to get a more accurate perception of how the app runs. This is the lowest-friction way to check it out!
To summarize the most important bits:
- You can generate a production build by running
npm run build
. - You can run a local production server by running
npm run start
. - Take care to stop your dev server before running either of these commands.
Port conflicts
Both the development server and the production server default to running on port 3000.
The development server is flexible. If port 3000 is taken, it'll try 3001. Then 3002. It keeps going until it finds an available one:
The production server, annoyingly, is not as flexible. It'll complain if something is already running on the default port:
If something else is already running on port 3000, the process throws an error, rather than trying the next available port.
As mentioned, you want to stop the dev server before starting a production server, and so this won't be an issue if you're only working on a single project. But if you work on multiple projects at the same time, you'll run into this issue.
For example, when I'm working on this course platform, I run the dev server on port 3000. Then, I start my blog, which runs on port 3001. If I want to check how my blog runs in production, I'll kill the blog's dev server, but the course platform is still running on 3000.
Fortunately, there's a workaround:
// package.json{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "start:local": "next start -p 4004", "lint": "next lint" },}
I've created a new NPM script, start:local
. It does the same thing as the standard start
script, but it specifies a custom port using the -p
flag. In this case, I've chosen port 4004, but in practice, I pick a different number for each project, to avoid conflicts.
Why not edit the existing start
script? As we'll cover in the next lesson, the start
script will also be used when we deploy the site, on the server. If we edit this script, we might break our deployments. So it's better to create a separate script we can use exclusively on our local machines.
Analyzing our bundles
Video Summary
When we run a build, Next will show us a high-level overview of our bundles:
This isn't super actionable, is it? It doesn't really tell us what's going on with these bundles. Let's see how we can use a package called webpack-bundle-analyzer
to glean some insights.
This tool provides a visualization of what's inside each bundle:
The boxes are drawn proportionally to their file size. This allows us to see which modules are taking up the most space inside our bundles.
This is an interactive tool. We can click in to dig deeper:
We can also filter based on the “entry point”, which allows us to see the bundles for each route. For example, here's the bundle for our homepage:
This view is great for getting insights about our bundles. We can see if any packages are being included when they shouldn't be. If a module is taking up too much space, we can look for a lighter alternative on NPM, see if we can remove it altogether, or lazy load it.
This is all made possible with the @next/bundle-analyzer package (opens in new tab). You can find up-to-date instructions on the package's NPM page, but I'll show you what I did to set it up.
After installing the package, I copied this chunk of code into my next.config.js
:
const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true',});
module.exports = withBundleAnalyzer({});
This includes the code to run the analysis, but only when the ANALYZE
environment variable is set to true. This prevents the analysis from running when doing a standard build (which would be a waste of time).
Then, I added a new NPM script to my package.json
:
{ "name": "sample-next-app", "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "analyze": "ANALYZE=true npm run build", "lint": "next lint" }}
When we run npm run analyze
, it sets the ANALYZE
environment variable, and then runs a standard build. This will automatically open the relevant HTML file in-browser, once the build is completed.
Note: The process is a bit different for Windows users. See my note at the bottom of this lesson.
You can learn more about this tool by visiting the NPM package page:
Honestly, I don't use this tool super often, but whenever I do use it, I find it incredibly useful. I think it's a good idea to check it every few months, to look for any low-hanging fruit.
Assessing NPM packages
Let's suppose we're building an app that requires a rich-text editor. We go looking on NPM, and come up with several options: draft-js (opens in new tab), react-rich-text-editor (opens in new tab), react-quill (opens in new tab), etc.
One of the important questions to answer as part of this process is: how big is the library? How many kilobytes will it add to our bundles?
My tool of choice for this is Bundle Phobia (opens in new tab). Plug in the name of the package, and it'll show you how many kB's it would add to your bundle.
Now, it's not 100% accurate. It's impossible for any tool to know exactly what sort of impact a dependency will have in your specific application. There are a few reasons for this:
- You might already be using some of the sub-dependencies needed by the package. If react-quill depends on
left-pad
but you're already usingleft-pad
in your app, that dependency will be reused. - On the other hand, some packages require peer dependencies that are not counted by Bundle Phobia.
- Some modules support “tree-shaking”, which means that you only pay for the specific parts of the library you use. For example,
react-feather
is 26kb if you use every single icon, but on my blog, it only adds 1kb-6kb, depending on the route.
And so, if you really want to know how a given package will impact your bundle size, you'll need to do the bundle-analyzer process described above, running a build with/without the dependency to see the difference. But I've found that Bundle Phobia (opens in new tab) is a great way to get a rough answer with a lot less effort.