Next.js (SSR) vs. Create React App (CSR)
Exploring the performance differences between server-side-rendering (SSR) and client-side-rendering (CSR) through simple examples.

First, you need to understand the basics of SSR and CSR:
The main difference is that for SSR your server’s response to the browser is the HTML of your page that is ready to be rendered, while for CSR the browser gets a pretty empty document with links to your javascript. That means your browser will start rendering the HTML from your server without having to wait for all the JavaScript to be downloaded and executed. In both cases, React will need to be downloaded and go through the same process of building a virtual dom and attaching events to make the page interactive — but for SSR, the user can start viewing the page while all of that is happening. For the CSR world, you need to wait for all of the above to happen and then have the virtual dom moved to the browser dom for the page to be viewable.
— Walmart Labs — The Benefits of Server Side Rendering Over Client Side Rendering
We used the popular Next.js library for the SSR solution and the Create React App (CRA) library for the CSR solution.
The Comparison
We explore the performance characteristics of the scaffold example provided by CRA; consisting of an image and some custom styling. We implement the same example with Next.js.

note: The measurements are taken using Chrome Browser through a simulated Fast 3G network; used to highlight any performance differences.
The first load; with nothing cached:
Create React App (CRA)

Next.js

Observations:
- The display of non-image content is delayed with CRA (2 seconds) as compared to Next.js (< 1 second); with CRA the non-image content cannot be displayed until all the JavaScript files (largest is 109 KB) are downloaded
- The display of image content is delayed with CRA (2.8 seconds) as compared to Next.js (2 seconds); with CRA the images cannot be downloaded until all the JavaScript files are first downloaded
- In both examples, the application becomes interactive once all the JavaScript files are downloaded; CRA (2 seconds) and Next.js (2.8 seconds). The largest Next.js JavaScript file is 173 KB (larger than the 109 KB for CRA)
For the subsequent loads; with everything cached (favicon does not matter):
Create React App (CRA)

Next.js

Observations:
- The display of non-image content and the application becoming interactive is the same with both examples (< 1 second)
- The display of image content is delayed with Next.js (1.3 seconds) as compared to CRA (< 1 second); with Next.js the images are not cached
The Critical Issues
For CRA the size of the largest initial JavaScript bundle is important during the first load for both the display of all content and for the application becoming interactive.
For Next.js, during the first load, the size of the largest initial JavaScript bundle is only important for the application becoming interactive; the display of all content is mostly unaffected by it.
With Next.js, during subsequent loads, the images are substantially delayed because they are not cached by design as described in a Next.js issue:
@dbo since we don’t hash static files, we are not possible to do that.
It’s a pretty good idea to move your static content into a CDN or something similar and avoid /static as possible as you can.We should improve this behavior in the future. But it’s not happening anytime sooner.
— Contributor — Set proper Cache-Control max-age for static assets
The Solutions
For both CRA and Next.js the size of the largest initial JavaScript bundle can be equally minimized using bundle splitting.
For CRA, code-splitting is something one has to deliberately implement; albeit it is fairly easy to do (especially if one uses the react-loadable library).
One advantage of Next.js is that by its design, it encourages bundle splitting by automatically splitting based on the pages feature (something one is likely to use).
Conclusion (Updated Conclusion Below)
It is important for both CRA and Next.js that the largest initial JavaScript bundle to be be minimized (we want our applications to be interactive). In both cases this is easy to accomplish using bundle splitting. In the case of Next.js, however, bundle-splitting happens automatically in common situations (easier for beginners to get right).
Next.js, by design, serves up static files to be not cached. To be honest, this is a bit of a deal-breaker for me (have not found a good solution here).
Finally, there is the obvious distinction in that the output of CRA (static files) can be served up through any web server (say a CDN) while the output of Next.js (being server rendered) is served up by a Node.js server (going to be substantially slower than a CDN).
Alarmingly, the proponents of SSR make an interesting observation.
SSR throughput of your server is significantly less than CSR throughput. For react in particular, the throughput impact is extremely large. ReactDOMServer.renderToString is a synchronous CPU bound call, which holds the event loop, which means the server will not be able to process any other request till ReactDOMServer.renderToString completes. Let’s say that it takes you 500ms to SSR your page, that means you can at most do at most 2 requests per second. *BIG CONSIDERATION*
— Walmart Labs — The Benefits of Server Side Rendering Over Client Side Rendering
In thinking this through some more, the only way I see around this limitation is to run a lot of Next.js servers through a load-balancer (a pretty complicated dev-ops problem).
Addendum: Got a helpful response on how others are deploying Next.js using server-less solutions, e.g., AWS Lamda, Google Cloud Functions, or Zeit Now. Interestingly enough, Zeit is the company that hosting Next.js on their GitHub account.
1/11/19 Addendum: As it is relevant and documented in the CRA documentation; added a section on pre-rendering with CRA.
Pre-Rendering with Create React App
More recently, I stumbled (or someone pointed it out) onto a CRA documented feature.
If you’re hosting your build with a static hosting provider you can use react-snapshot or react-snap to generate HTML pages for each route, or relative link, in your application. These pages will then seamlessly become active, or “hydrated”, when the JavaScript bundle has loaded.
— Create React App — Pre-Rendering into Static HTML
Following the instructions provided in react-snapshot; just changing one line in each of two files.
Running the same tests as we did earlier:
The first load; with nothing cached:

Observations:
- The display of non-image content is delayed with Pre-Rendered CRA (1.5 seconds) as compared to Next.js (< 1 second); with Pre-Rendered CRA the non-image content cannot be displayed until the CSS file (1 KB) is downloaded. Important: These tests were run using a local test server (http-server) and assume that with using a fast CDN a 1 KB file would load a lot faster; putting it on par with Next.js
- The display of image content is delayed with Next.js (2 seconds) as compared to Pre-Rendered CRA (1.5 seconds); with Next.js the images cannot be downloaded until one of the initial six simultaneous files is completed its download
- The application becomes interactive once all the JavaScript files are downloaded; Pre-Rendered CRA and CRA (2 seconds) and Next.js (2.8 seconds). The largest Next.js JavaScript file is 173 KB (larger than the 109 KB for CRA)
For the subsequent loads; with everything cached (favicon does not matter):

Observations:
- The display of non-image content and the application becoming interactive is the same with both examples (< 1 second)
- The display of image content is delayed with Next.js (1.3 seconds) as compared to Pre-Rendered CRA (< 1 second); with Next.js the images are not cached
Conclusion (Updated)
Using Pre-Rendered CRA addresses both the SEO and performance concerns around using CRA without the complexity that Next.js introduces, e.g, still can serve up the application as static files. The CRA recommended article, An Almost Static Stack, gives more detail around how to to using react-snapshot; including multiple endpoints (with React Router).
For those using TypeScript, react-snapshot does not supply type definitions (neither is there a DefinitelyTyped version). The good news is that because it is just a drop-in replacement for react-dom, it can be easily typed; just drop the following at the end of your:
./src/react-app-env.d.ts
...
declare module 'react-snapshot' {
import * as ReactDOM from 'react-dom';
var render: ReactDOM.Renderer;
}
Enjoy!