What are the advantages of using Next.js?

The Next.js framework has been through a few major releases in the past couple of years and has now emerged as one of the top choices for React development.
Since its last couple major releases, Next.js has definitely gained a lot of interest and in this article we'll take a look at some of the advantages of using Next.js.
After building several Next.js apps over the past couple of years and helping large scale clients migrate to this framework, I hope you'll find this article informative.
What is Next.js?
Next.js is a next generation React framework built on top of the SWC Rust-based platform which is significantly faster than Babel.
It features all of the functionality to build highly scalable React apps while streamlining the development process.
In addition, it simplifies the Server Side Rendering (SSR) and Static Site Generation (SSG) process which improves page loading performance.
Why is Next.js important?
One of the key advantages of using Next.js is that it is showing us a glimpse of the future of React. Next.js is very much aligned with the React development team and bringing us full implementations of the React roadmap.
Advantages of using Next.js
Long before JavaScript was ever really a fully robust front end language, it was merely a scripting language introduced by Netscape in the mid 90's.
All web page content was fully rendered on the server side using CGI/Perl, PHP, ASP and Java. Node.js wouldn't hit the scene for at least another 10 years.
Since that time, JavaScript frameworks like React have come full circle and are now capable of rendering on the server side without the complexity it carried before and frameworks like Next.js are making it easier than ever before.
So Next.js is bringing the capabilities of what frameworks built on PHP have been doing for years but using React end to end without the need for a separate server.
Next.js is highly geared for SSR and SSG which brings high performance gains on page load times.
React Server Components (RSC)
You'll definitely want to get familiar with this term because it's definitely well supported by Next.js and also a specification by React.
Since its introduction and full implementation of RSC by Next.js in version 13+, it has greatly simplified the way we think about SSR.
What is React Server Components (RSC)?
We no longer have to think of React SSR as rendering a full page anymore, we have moved from page level, to the component level with RSC. We can now think of components as either server or client side components.
RSC components are rendered completely on the server side, so there's no browser runtime context, like document
or window
objects.
The ideal way to use RSC components, include:
- Render and output as much static HTML content as possible in these components.
- Put the responsibility of API fetching and rendering inside these components.
- Pass and cascade fetched data as props to
client
components so the UI can focus on presentation and interactivity an not loading and computations.
All components in Next.js are by default server
components unless specified with the use client
declaration.
'use client'
export default function Page() {
return (
<main>
<h1>Client Component</h1>
</main>
);
}
Next.js fully supports RSC as of version 13. You can find more information on Server Components here.
File-based routing
One of the largest developer benefits has to be the move to file-based routing. Although most other frameworks have also moved in this direction, Next.js offers some additional perks.
What is file-based routing?
Introduced in version 13, is the new /app
router directory. The previous /pages
directory is still supported but it's best to migrate to the new app
router for full benefits of SSR and SSG using RSC.
File-based routing allows you to easily build intuitive routes using directories and files. There's no need to use the React router to build routes.
By using certain file naming conventions, it instructs Next.js on how to build route paths. For example:
page.tsx
: indicates a page component.
layout.tsx
: indicates a layout component.
template.tsx
: indicates a layout component with re-render capability.
loading.tsx
: used for loading state progress.
error.tsx
: automatically adds error boundry.
404.tsx
: automically handles 404 error codes.
These are just a few of the reserved file names to create routes.
Examples of Next.js file-based routes
Here's an example of a route path, consider the following routes.
/products/product/1
/products/product/1200
/support/warranty
The app
router will split a path into segments using the /
delimiter. The products route path can use a dynamic [id]
param to match any product ID.
Since the last path segment is a directory and is named with a dynamic [id]
param, it will render the page.tsx
, since it is a leaf node and has no subdirectories.
The support page would match an exact route path without any path params, since the last path segment is a file page.tsx
and has no subdirectories.
└── project-root
└── app
├── products
│ ├── product
│ │ └── [id]
│ │ └── page.tsx
│ └── layout.tsx
├── support
│ ├── warranty
│ │ └── page.tsx
│ └── layout.tsx
└── layout.tsx
The app
router takes into account any layout.tsx
files it finds in directories along the path and will nest the layouts until it reaches a page.tsx
(leaf node) and renders a standard React functional component.
All layout.tsx
rendered content and fetch
calls they make are cached to improve performance. If you need a more dynamic layout component that can re-render, you can use template.tsx
in certain cases.
The first layout in the path would be the root base layout which defines the HTML page and then wraps children
.
/layout.tsx
export default function Layout({ children }: {
children: React.ReactNode;
}) {
return (
<html>
<head><title>Some Page Title</title></head>
<body>{children}</body>
</html>
);
}
The second layout could further define the product page layout and would wrap the children
/products/layout.tsx
export default function Layout({ children }: {
children: React.ReactNode;
}) {
return (
<section className="products">
{children}
</section>
);
}
The last page.tsx
path segment would define the actual page content. It will have access to the params
and searchParams
in the props.
/products/product/[id]/page.tsx
export default function Page({params}) {
return (
<div className="product">
<h1>Product {params.id}</h1>
</div>
);
}
And the final rendered HTML output would be as follows:
<html>
<head><title>Some Page Title</title></head>
<body>
<section className="products">
<div className="product">
<h1>Product 1200</h1>
</div>
</section>
</body>
</html>
So as you can see this makes it very easy to develop page routes without having to do all the mapping. If you want to change the paths of the routes, you can simply rename the directories.
Here's an in-depth article on building Next.js Layouts for maximum flexibility where I cover some of the strategies I use when I build Next.js apps.
File-based API routes
Much like page routes, Next.js offers full support for file-based API endpoints making it simple to create quick endpoints as needed.
What are file-base API routes?
File-based API routes follow a similar path segement naming convention as page routes. Any path under the /app/api
directory is an API route provided that the last path segment is named using route.tsx
.
Any route.tsx
can export one or more of the following named export functions.
- GET: creates a get method HTTP endpoint.
- POST: creates a post method HTTP endpoint.
- PUT: creates a put method HTTP endpoint.
- DELETE: creates a delete method HTTP endpoint.
- PATCH: creates a patch method HTTP endpoint.
- HEAD: creates a head method HTTP endpoint.
- OPTIONS: creates a options method HTTP endpoint.
For example, to create a GET
enpoint to retrieve some products you can create the following file path.
/products
└── project-root
└── app
└── api
└── products
├── route.tsx
└── [id]
└── route.tsx
Here's a typical CRUD API endpoint example.
/app/api/products/route.tsx
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
try {
const result = await fetchProducts();
return NextResponse.json({ message: "OK", result }, { status: 200 });
} catch (error) {
return NextResponse.json({ message: "Error", error }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const body = await request.json();
try {
const result = await saveProduct(body);
return NextResponse.json({ message: "OK", result }, { status: 201 });
} catch (error) {
return NextResponse.json({ message: "Error", error }, { status: 500 });
}
}
export async function PUT(request: NextRequest) {
const body = await request.json();
try {
const result = await updateProduct(body);
return NextResponse.json({ message: "OK", result }, { status: 201 });
} catch (error) {
return NextResponse.json({ message: "Error", error }, { status: 500 });
}
}
Using the [id]
path param we can get a specific id
and delete by the same.
/app/api/products/[id]/route.tsx
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest, {params}) {
const id = params.get("id");
try {
const result = await fetchProductById(params.id);
return NextResponse.json({ message: "OK", result }, { status: 200 });
} catch (error) {
return NextResponse.json({ message: "Error", error }, { status: 500 });
}
}
export async function DELETE(request: NextRequest, {params}) {
const id = params.get("id");
try {
const result = await deleteProductById(params.id);
return NextResponse.json({ message: "OK", result }, { status: 200 });
} catch (error) {
return NextResponse.json({ message: "Error", error }, { status: 500 });
}
}
Edge Runtime.
Using a file-base approach allows for quick API endpoint creation without the need for a third party API server like Express or something similar.
The other advantage is that all routes under the /app/api
directory are exported functions and Edge Runtime ready when deployed on Vercel.
SSR and SSG rendering
Since all component modules in Next.js are RSC by default, they are fully rendered on the server side, however, you can also statically render your pages at build time for optimal performance.
What is Server Side Rendering (SSR)
SSR is simply the process of rendering React components and modules on the server side and returning an HTML string output.
Before Next.js, React had some helper functions to do the job.
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
You would typically run this code in an Express endpoint and return a response with an HTML string to the browser.
The problem with this approach is that it's not taking into account any code splitting.
So things improved with libraries like react-universal-component
and react-loadable
which would use Webpack to split the code into chunks and bundle it so you can lazy load it.
Example of loading with universal
import universal from 'react-universal-component'
const UniversalComponent = universal(props => import(`./${props.page}`))
export default () =>
<div>
<UniversalComponent page='Foo' />
</div>
React also had React.lazy
and Suspense
to load components. However, even this became quite hard to manage and required a good amount of Webpack experience.
The solution to the problem was RSC, because it takes all of the complexity out of the equation and allows developers to focus on what they do best and that's UI code development.
So, SSR in Next.js is just a matter of properly using server
and client
components effectively to maximize how much HTML content can be rendered on the server.
There should be a fundamental shift in how a React developer thinks about building apps on Next.js. Here's a simple example.
/app/demo/page.tsx
import { ClientBanner } from '@/components/banner';
import { ClientMenu } from '@/components/menu';
export default async function Page() {
const banner = await getBannerData();
const menu = await getMenuData();
return (
<main>
{}
<ClientBanner banner={banner} />
<ClientMenu items={menu.items} />
</main>
);
}
A simple banner client
component.
/components/banner
'use client';
type BannerProps = { banner: { href: string; image: string } };
export function Banner( { banner: {href, image}}: BannerProps) {
return <div><a href={href}><img src={image}></img></a></div>;
}
A simple menu client
component using React hooks.
/components/menu
'use client';
import { useState } from 'react';
type MenuItem = { label: string; href: string };
type MenuProps = { items: MenuItem[] };
export function Menu({ items }: MenuProps) {
const [showMenu, setShowMenu] = useState<boolean>(true);
return (
<main>
<ul onClick={() => setShowMenu(!showMenu)} className={`${showMenu ? 'block' : 'hidden'}`}>
{items.map((item, i) => (
<li key={`${i}-item`}>
<a href={item.href}>{item.label}</a>
</li>
))}
</ul>
</main>
);
}
What is Static Site Generation (SSR)
The major difference between SSR and SSG is that SSR will have a mix of server
and client
componets and will render any server content as pure HTML first and then stream the client content as it's ready.
In SSG, all of the rendering is done at build time and is completely static HTML making the page super fast when it loads.
So things like blog, documentation and website content will be fully optimized as static HTML by the Next.js build process.
Next.js has some server side functions like generateStaticParams
for routes that have dynamic params like [id]
or [...slug]
.
It will use the generateStaticParams
function to generate all the params for each of the pages to render the content.
You can find more information here
Server Actions
Another advantage that is kind of exciting, is the ability to use Server Actions
on HTML form submits.
These are simple functions that are called on the client but run on the server without the need to create an API endpoint.
What is a Server Action
By exporting an async function using use server
, it can now be called and run on the server. You can define multiple actions in one file as long as the use server
is declared at the top.
Creating Server Actions
/components/actions/profile.ts
'use server';
async function createProfileAction(formData: FormData) {
const profile = {
name: formData.get('name')
};
}
async function updateProfileAction(formData: FormData) {
}
async function deleteProfileAction(formData: FormData) {
}
Invoking a Server action
Now, simply import the function and call it using the action
attribute on the form.
'use client'
import {createProfileAction} from '@/components/actions/profile';
export default function Page() {
return (
<form action={createProfileAction}>
<input type={'name'} />
<button type="submit">Save</button>
</form>
);
}
Next.js will automatically create a FormData
object and pass it as the argument to action function.
You can find more information on Server Actions here
SEO Optimization Support
The other area that Next.js has great support for is SEO optimization, which includes:
- Page Metadata support for social media platforms and other important meta tags.
- A new
<Head>
component for header support.
- A new
<Image>
component to optimize loading and to avoid content layout shift (CLS)
- Use a
sitemap.ts
page to generate sitemap.xml files.
- Use a
robots.ts
page to generate a robots.txt file.
These are just some of the areas it provides support for. Here's a full article on SEO Optimization where I go into more detail on the subject.
Local and Remote MDX support
Next.js has full support for the MDX technology, which combines simple HTML Markdown syntax with React components.
It is quickly becoming a great way to build static HTML content pages with the ability to add interactive React components.
If you've ever written a README.md document, then you may have some idea of the Markdown syntax.
- The
app
router has full support for local MDX using page.mdx
extensions.
- It also supports
next-mdx-remote
, which is a library for remote MDX which is RSC capable.
Here's a couple recent articles on this very topic.
Next.js uses Tailwind CSS
Tailwind CSS is the default CSS framework Next.js uses but it can use any other CSS framework you might need. However, there are some good reasons to use a framework like Tailwind CSS.
Why use Tailwind CSS on Next.js
Tailwind CSS generates CSS utility classes at build time, which makes it a zero-runtime CSS framework.
In other words, The browser doesn't have to dynamically generate CSS on the fly like CSS-in-JS frameworks do. So it makes it an ideal framework for SSR and SSG rendering.
Here's some of the main benefits:
- SSR and SSG friendly
- Quickly build applications without naming CSS classes.
- Can still write plain CSS if needed
- Can write custom plugins.
- Large community support
- Many third party plugins and themes
In Conclusion
This article probably just touched the surface of the benefits but I think Next.js is one of the top frameworks for React development. It really simplifies the pain points of traditional React application development.
I've written several Next.js apps over the years and I highly recommend taking a closer look if you want to continue using React in a much more streamlined way of developing.
I hope this article was informative and has given some ideas to help make your next decision.