Editor’s note: This post was updated on 28 July 2022 to reflect the current versions of Next.js and Create React App, compare performance differences in image optimization and code splitting, review some Next.js limitations, and provide information about migrating between Create React App and Next.js.
Next.js vs. Create React App: What’s the best way to improve single-page application performance? In this article, we will unpack that question with some data. But first, we need to understand what exactly we are comparing.
We will cover:
The first step towards understanding the performance differences between Next.js and Create React App is to understand the frameworks themselves. Let’s learn a little more about each option and define important terms such as SSR and CSR.
Next.js is a React framework built by Vercel (formerly Zeit) to make server-side rendering (SSR) easier for React applications regardless of where your data comes from.
Next.js also supports static exporting and, newly, incremental static regeneration. However, for the purposes of this post, we will focus on its server-side rendering (SSR) capability.
Create React App is officially supported by Meta for creating single-page applications (SPAs) for React. Again, for the purposes of this post, we are paying attention to the term “single-page.”
Next.js is one way that you can leverage React to support server-side rendering (SSR). Likewise, Create React App is one way that you can leverage React to support client-side rendering (CSR).
There are other frameworks out there when it comes to either choice, but what we are really comparing in this post is how each rendering strategy impacts web application performance. We just happen to be using two of the more popular frameworks out there to make that comparison.
Let’s start our experiment with just one of many other questions we can ask when comparing Next.js vs. Create React App: Does SSR improve application performance?
Walmart Labs published a great post titled, “The Benefits of Server Side Rendering Over Client Side Rendering.” They also provide some excellent diagrams that demonstrate the fundamental difference between SSR vs. CSR performance.
Remember, you can support SSR in your React applications with frameworks like Next.js, while frameworks like Create React App enable you to support CSR.
These diagrams postulate that SSR can deliver HTML to the browser faster than CSR can. So, let’s hypothesize that a web application built with SSR is more performant than one built with CSR.
Note that the Walmart Labs article calls out synchronous SSR as a bottleneck with renderToString
. Starting with React 16 and Fiber enhancements, this is no longer an issue.
The best way to test our hypothesis is by building two applications with identical functionality and UI. We want it to mimic a real-world application as much as possible, so we will set a few parameters.
The application must:
Software developers are typically spoiled with high-powered computers paired with blazingly fast office networks; we do not always experience our applications the same way our users do.
With that in mind, when optimizing for performance, it is important to consider both network and CPU limitations. Mobile devices generally have less processing power, so heavy JavaScript file parsing and expensive rendering can degrade performance.
Fortunately, Chrome provides a dev tool called Lighthouse, which makes it easy for us to step into the shoes of our users and understand their experience. You can find this under the “Lighthouse” panel in Chrome DevTools.
We will use the exact settings displayed above:
If you live in Northern California, you are likely on servers living in AWS Region us-west-1 (N. California) all day. In that case, you are not experiencing your application the same way as users in other parts of the United States, nor other parts of the world.
So, for the purposes of this test, the demo apps and the API were deployed to Sydney, Australia (specifically, Vercel’s syd1 region). The client’s browser will be accessing the applications from Boulder, CO, USA.
The distance between Boulder and Sydney is 8,318 mi (13,386 km).
Look at what that means for data fetching between these two applications:
The image above illustrates the impact of geographical distance on CSR data fetching. The image below illustrates the same, but for SSR data fetching:
In order to avoid Chrome extensions negatively affecting our app’s page load performance, we’ll analyze its performance in incognito mode.
The code for the two apps is available in my GitHub. You can view the result of each application at the links below:
All of the code is in a monorepo:
/cra
contains the Create React App version of the application
/nextjs
contains the Next.js version
/api
contains a mock API that both applications use
The UI appears identical. Remember that the Create React App application has a CSR UI, while the Next application has an SSR UI:
And the JSX is nearly identical. See the Create React App code below:
// Create React App <ThemeProvider> <div> <Text as="h1">Create React App</Text> <PrimaryNav align="left" maxItemWidth="20rem"> <NavItem href="/" selected>Create React App</NavItem> <NavItem href="/nextjs">Next.js</NavItem> </PrimaryNav> <Table data={users} rowKey="id" title="Users" hideTitle /> </div> </ThemeProvider>
Now compare it to the Next.js code here:
// Next.js <ThemeProvider> <div> <Text as="h1">Next.js</Text> <PrimaryNav align="left" maxItemWidth="20rem"> <NavItem href="/">Create React App</NavItem> <NavItem href="/nextjs" selected>Next.js</NavItem> </PrimaryNav> <Table data={users} rowKey="id" title="Users" hideTitle /> </div> </ThemeProvider>
We will get to what the ThemeProvider
and other components are in a moment.
Notice that while most of the code above is the same for both Create React App and Next.js, the code differs in how the data is fetched from the API. See the Create React App code below:
// Create React App // This all executes in the browser const [users, setUsers] = useState([]); useEffect(() => { const fetchData = async () => { const resp = await axios.get('/api/data'); const users = resp.data.map(user => { return { id: user.id, FirstName: user.FirstName, DateOfBirth: moment(user.DateOfBirth).format('MMMM Do YYYY'), } }); setUsers(users); }; fetchData(); }, []);
And the Next.js code looks like so:
// Next.js // This all executes on the server on first load Index.getInitialProps = async({ req }) => { const resp = await axios.get(`http://${req.headers.host}/api/data`); const users = resp.data.map(user => { return { id: user.id, FirstName: user.FirstName, DateOfBirth: moment(user.DateOfBirth).format('MMMM Do YYYY'), } }); return { users }; }
getInitialProps
is a special function that Next.js uses to populate the initial data for a page in Next.js. You can learn more about fetching data in the Next.js docs.
So what’s with all these components, and why are we using Moment.js?
We are also pulling in Moment.js because it is a larger dependency that adds some JavaScript weight and also some additional processing that needs to occur when rendering the component tree.
The actual libraries that we’re using are not important; the point is to get a little closer to the weight of a normal application without taking the time to build all of that in its entirety.
Here are the Lighthouse results for a full page load on our Create React App (CSR) application:
And below, you can see the Lighthouse results for a full page load on our Next.js (SSR) application:
Based on the overall score, you can see that our Next.js application (SSR) performs better than our Create React App (CSR) application. To understand the details of these metrics, read the Lighthouse Scoring Guide.
One of the more notable differences for our purposes is the First Meaningful Paint. See the difference below:
According to Google’s First Meaningful Paint docs, this metric “identifies the time at which the user feels that the primary content of the page is visible.”
Lighthouse also helps us visualize these differences. Check out this comparison between Create React App (CSR, top) and Next.js (SSR, bottom):
Do the visuals above look familiar? They should, because they mimic the diagrams we looked at previously, when we postulated that SSR can deliver HTML to the browser faster than CSR. Based on these results, it can!
To view the Lighthouse results yourself, follow these steps:
When it comes to image optimization, CRA does not have much to offer. Next.js once again outperforms CRA in this case.
Next.js has a built-in image component called next/image
, which is a modified HTML <img>
element with a number of built-in performance optimizations for rendering images. It was introduced in version 10.
The next/image
component serves device-specific images using modern image formats. It also speeds up page loading by loading images only when they enter the viewport, with blurred-up placeholders as an option.
When it comes to performance, code splitting is another important factor to think about.
Splitting the application’s bundle into smaller portions that each entry point needs is known as code splitting. The goal is to reduce the initial load time of the application by loading only the code needed to run that page.
In order to perform code splitting properly with Create React App, the developer must put in a lot of effort and use additional libraries. It’s advisable to stick to the dynamic import()
syntax because it’s the recommended technique for managing code splitting in Create React App, and Webpack will immediately start code-splitting your app when it encounters it.
In Next.js, each file in the pages/directory
is automatically code-split into its own JavaScript bundle during the build stage; therefore, code splitting is enabled out of the box. Additionally, any code that is shared between pages is divided into a separate bundle to avoid re-downloading the same code on subsequent navigation.
Following the initial page load, Next.js can begin preloading the code for any subsequent pages that users are likely to visit. In a typical Create React App application, this will require lots of codes and sometimes additional libraries.
Although Next.js is a great framework, it has a few drawbacks in terms of development.
For example, Next.js has relatively high development complexity on the server side. Some JavaScript plugins and libraries, such as Bootstrap and other libraries that support DOM manipulation, are designed to run only on the client-side requiring extra effort from the developer to handle client- and server-side validations to determine when to import and use such libraries.
Additionally, Next.js requires a Node.js server to run JavaScript on the server side. Thus, you must first set up the server before you can use Next.js to create your app.
If you are considering migrating your existing non-ejected Create React App project to Next.js, the Next.js team has documented a step-by-step guide on migrating to Next.js in the official Next.js documentation.
We opened our experiment with a question: Does SSR improve application performance?
To investigate this question, we built two nearly identical applications, one that uses client-side rendering with Create React App and one that uses server-side rendering with Next.js.
The Lighthouse results from our simulations showed better metrics in the Next.js application in all significant categories, especially First Meaningful Paint (87.69 percent decrease), First Contentful Paint (87.69 percent decrease) and Time to Interactive (27.69 percent decrease).
The results speak for themselves. What has your experience been like with Next.js vs. Create React App?
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
24 Replies to "Comparing Create React App vs. Next.js performance differences"
One thing that I believe needs to be clarified is that Next.js takes advantage of isomorphic/universal rendering. When an app is loaded for the first time, it’s loaded via SSR. Afterwards, CSR kicks in, giving you the best of both worlds.
Jason, your info is very important because if you use a Server Side Rendering only approach instead of the isomorphic rendering like Next.js, you will still end up with a faster loading time in the SSR page but with slower website rendering after the initial load.
Jason, you are absolutely right. Good call out!
The linked Walmart Labs article is excellent but it’s important to remember it’s from 2017, and they call out synchronous SSR as a bottleneck with renderToString. This is no longer the case with React 16 and the enhancements that Fiber brought.
Interesting that Gatsby is not included in this. With it’s client side re-hydration it’s pretty solid for building web apps as well as static sites.
Recently built http://www.growable.io with Gatsby and the performance wins out of the box are crazy 🤯
Any reason why it wasn’t included?
Will, I did not include Gatsby because I believed it to be a static site generator. Static sites will be more performant, but this post was intended to cover applications that require dynamic data.
Whatever, how do you justify that excessive DOM size?
Interesting, thank you for sharing! I have experience with CRA but am considering Next.js for a new project. I did leverage SSR with CRA though, so I’d be interested to know how CRA with SSR compares to Next.js. My instinct says since the architecture is similar then the performance should be, but then the SSR for CRA is more home-rolled so likely not as optimized. I realize that would be a more extensive benchmark to build out though.
Matt, I suspect the performance difference would be negligible between CRA w/SSR and Next.js w/SSR. I personally prefer not to home-roll when there is a viable meta framework available like Next.js, but that choice is based more on developer ergonomics rather than performance.
This
I have a question here.
I’m already aware with the benefits of Nextjs on CRA. I migrated my website(https://m.truebil.com) from CRA to Nextjs. I got 80% improvement on FMP, FCP which also improve the lead conversion by 15%. This is awesome.
But this is all for new user. It has negative impact on returning user because CRA works better for returning users. For the returning user drop in lead is 10%.
CRA works better for returning user because all contents(js) are cached on client side so it get rendered faster whereas in SSR first call goes to server which is time consuming.
How can I improve the performance of returning user?
But you can’t deploy it (static exported) to Apache
Let me ask you how the deployment looks like ? with CRA you just need a bucket , but if I understand next.js correctly you will need a nodejs server running, so much more expensive and resource intensive . Another thing is the next js page redoing the query on each request ? I think getinitial props don’t do it, but getserverprops does it, can you confirm the behaviour
At the time this was written, these were deployed to Vercel (formerly Zeit). With server rendering, you do need a Node.js server though I recommend using Vercel or Netlify serverless options. It will keep your costs low and with Next.js you can now deploy hybrid apps where you only server render when you need to. Static pages can be deployed as static HTML, which can then be served by a CDN.
getInitialProps will refetch on every request as long as you aren’t using the static export option. Use Next.js standard build/deploy and you’ll be able to choose between static/server rendering per page.
It would have been nice to see a comparison between both a worst and best case scenario. Would a SPA not generally see better performance when navigating between different pages than a SSR website? I think SSR: good, CSR: bad is a gross oversimplification as there are pros and cons to both solutions.
As another reader commented, Next.js uses CSR after the initial page load. The exercise in this article was purely focused on initial load but it is important to note that Next.js’s hybrid approach minimizes the trade-off since you don’t lose the benefits of CSR for subsequent page navigations.
Sean Connolly is wrong when he says “don’t lose the benefits of CSR for subsequent page navigations”. You do lose some benefits of CSR because when you navigate to the new page. Next JS’s bundle splitting still requires you to make another trip to the Next JS server to load new Javascript code (code that has other dependencies, determined by the build process, than code on initial load page) when you navigate new page.
Hey. Trying, thanks for clarifying that detail for others.
It’s important to understand that route based code splitting is also encouraged in CRA apps so they would have the same hit in route changes. With Nextjs it is trivial to prefetch that bundle to eliminate the delay.
Hi Sean,
Even when you also have Route splitting in CRA. Both sides are not equal.
As Vaibhav Kumar already mentioned, Next JS is bad for returning clients.
The reason why? JS bundle CACHING.
When configured correctly, the JS bundle will be cached. This means for returning clients, the next page of CRA will be loaded instantly at least with the skeleton. However, with Next, you always have to return to the server because … well.. the server… renders.
This is especially true and unavoidable when the data is dynamic. Yes you can still hack into the code and make the data fetching happens on client and renders skeleton while the server… renders. But it makes the code utter hell and complicated.
It is so disappointed to me that most people on the internet think Next is good. In my opinion, with prerender.io (they have free opensource for on premise setup), you don’t even need Next for SEO. The performance of Next may be a tiny bit better with initial page load but lose quickly for subsequent pages as well as returning clients (where JS bundle CACHING kicks in). So performance wise, Next is likely even a loser, no where near clear winner. On top of that, you have to setup your node server, wasting computing resource and cloud money, topped that with all the complications when using redux and CSS in JS. Next is bad conceptually in my opinion in everyway.
PS: No flame war intended. I just want to clear things out for a lot of junior, middle+ developers (I met a lot) when they say Next is new technology and good without having a deeper understanding of how everything cliques. If I understand anything wrong, please correct me. I will be grateful of your correction. Regards.
One suggestion:
– Can you make the CRA renders the Skeleton (https://material-ui.com/components/skeleton/)?
This will make the first useful content paint of CRA appears much quicker. This will show the true performance difference between CRA and Next JS in this first useful content paint category. Your current setup gives tremendous bias to Next JS.
One admission:
– After thinking about it, making Next show Skeleton instantly might not be very tricky as I thought. Please let us know how you would do that?
By the way, there is a huge bias in your test setup.
Your CRA bundle, css and html build was not served as STATIC assets from something such as Amazon Cloudfront.
If you serve it from Cloudfront, the empty/skeleton html result will comes from somewhere very close to the user in Boulder CO. And you will have first contenful paint with the skeleton (instead of empty) almost instantaneously with Cloudfront edge servers. The Lighthouse score would have been totally different.
Please publish our discussion. I don’t want to start a Next JS is bad article (flame war baiting accusation, you know) therefore I want to limit my opinion to comments. But at the same time, I want to point my junior devs to somewhere (such as this thread) to argument and discussion with you that explains why Next JS being faster than CRA is a myth. That way I don’t have to spend time explaining to them repeatedly. Thank you.
Hi Trung,
I understand the implications of bundle caching for returning users. But we need to emphasize the difference between displaying a skeleton vs displaying meaningful content. If you need to optimize the display of meaningful, dynamic data then a server render will still win.
If you want to prioritize the display of a skeleton you can go with either CRA or Next, because with Next you can generate static pages in addition to serve rendered pages. I prefer the flexibility Next provides.
If you want to provide your juniors with a helpful resource, I suggest you do some more research and write your own post.
Hi Sean Connolly, I am a junior developer about React. I have read your meaningful article and I have also read your comment when reply other opinions about this. I see you prefer to use Next rather than CRA even though you will create a complex web app? is that right?
I hope you can reply my question. I am sorry for my bad english language.
Thank you before.
Hi there! It’s been a while since this article was originally published. Next.js has come a long way since then and today I believe it’s a much simpler starting point than CRA is. Over time, many apps will grow in complexity and Next.js has enough flexibility to scale to those needs that might be difficult to foresee when the app is new. I can’t say the same for CRA as I think there are too many limits to its capabilities (personally I don’t think it was ever really intended to be a solution for long lived production apps).
The complexity comments for Next.js are usually tied to its specific server rendering capabilities as you need to account for the reality that your code can run in different contexts, i.e. in the browser and on the server and there are constraints associated with that.