Next.js is like React with benefits, in that it delivers all the features of React with ease-of-use conventions and a well-defined client-server stack. Next.js 13 is the newest version, released by Vercel at the Next.js conference in October 2022. It brings a slew of new features, including a bundler called Turbopack and support for several React-incubated optimizations like React Server Components and streaming rendering.
All told, Next.js 13 is a significant milestone, bringing together advancements in React and Next itself in a pleasantly usable developer experience package. This release also packs in considerable behind-the-scenes optimization. Let’s take a tour of what’s new in Next.js 13.
The new Turbopack bundler
Turbopack is written in Rust, which seems to be the go-to choice for systems-oriented tooling these days. Rust’s inherent speed is one reason underlying Turborepo’s performance as compared with other build tools. (Rust is something like C++, but with more memory safety.) Interestingly, the bundler space has been very active lately, with the Vite build tool gaining mindshare as the successor to Webpack. Vite is written in Go, a language of similar vintage to Rust. But Rust seems to have the edge on efficiency.
Turbopack also has architectural changes like clever use of caching, which improves the handling of source changes in an optimized way. The basic premise is that Turbopack holds a granular model of the changes that were already built and only builds those that are required to reflect ongoing changes.
Using the Turbopack in Next.js 13
It’s easy to create a new Next.js app via
create-next-app and use Turbopack. You use the
--example switch and give it the
with-turbopack argument, as shown in Listing 1.
Listing 1. Start a new turbopack-built Next App
npx create-next-app --example with-turbopack
If you look at
package.json, you’ll see this is reflected in a small change in how the
dev script works:
Listing 2. Dev mode script with turbo
"dev": "next dev --turbo"
Turbopack works as an opt-in replacement for Next.js’s devmode server for the moment, but there are big plans on the horizon, including frameworks beyond React. Svelte and Vue have both been mentioned by name. Presumably, Turbopack will become the default devmode tool, and also the production build tool at some point in the future.
When you run
npm run dev, you’ll see a screen like the one below.
Although you can see the impact of Turbopack’s performance most in large-scale apps, a fun little experiment shows the difference. Try running
--turbo enabled versus without, as shown in Listing 3. As you can see, even for the humble starter app, the start time drops from over 1000 milliseconds to around 8.
Listing 3. Turbopack devmode start time
// with --turbo ready - started server on 0.0.0.0:3000, url: http://localhost:3000 event - initial compilation 7.216ms // without --turbo ready - started server on 0.0.0.0:3000, url: http://localhost:3000 event - compiled client and server successfully in 1118 ms (198 modules)
The new /app directory
Now let’s look at our directory layout, where you will notice the new
/app directory. This is a new feature of Next.js 13. Basically, everything in the
/app directory participates in the next generation of React and Next.js features.
/app directory lives next to the familiar
/pages directory and supports more advanced routing and layout capabilities. Routes that match in both
/app will go to
/app, so you can gradually supersede existing routes.
The basic routing in
/app is similar to
/pages in that the nested folders describe the URL path, so
localhost:3000/foo/bar in our
Enabling the /app directory
/app directory is still a beta feature, so to use it you have to enable experimental features in
next.config.js, like so:
Listing 4. How to enable experimental features
experimental: appDir: true
Note that this was done for us when we scaffolded the new project with
Layouts in Next.js 13
One of the superpowers
/app has over
/pages is support for complex nested layouts. Every branch in your URL hierarchy can define a layout that is shared with its children (aka, leaf nodes). Moreover, the layouts preserve their state between transitions, avoiding the expense of re-rendering content in shared panels.
Each directory considers the
page.tsx/jsx file as the content, and
layout.tsx/jsx defines the template for both that page and the subdirectories. So, creating nested templates becomes simple. Moreover, the framework is smart enough to not re-render sections of the page that don’t change, so navigation will not repaint layouts that aren’t affected.
For example, let’s say we wanted to have a
/foo/* directory where all the children are surrounded by a white border. We could drop a
layout.tsx file into a
/app/foo directory, something like Listing 5.
Listing 5. app/foo/layout.tsx
export default function FooLayout( children ) return <section style=borderWidth: 1, borderColor:'white'>children</section>;
In Listing 5, notice the component destructures the “children” property and uses that to place the content inside the template. Here, the layout is just a section property with an inline style giving a white border. The page file in
/app/foo will have its contents rendered where the
children token is found in the layout file. By default, layout files create nested templates, so a route that matches a subdirectory of
/app/foo/* will also have their content placed within the
children element of
React Server Components
By default, all components in
Sometimes, when using client-side hooks like
useEffect (where the server can’t do the work beforehand), you need to tell React it’s a client-side component. You do this by adding a
‘use client’ directive to the first line of the file. We’ve previously used filename extensions like
.server.js to designate a client component that uses client-side hooks, but now you must use the
‘use client’ directive at head of
Streaming render and suspense
Streaming is another newer React optimization enabled by the new concurrent render engine. It’s a fundamental change in how the React engine works. The basic idea is that the UI can be divided into sections, and sections that depend on data can define loading states while they load the data concurrently. Meanwhile, sections that do not depend on data can receive their content right away for immediate display.
You will primarily use this feature with the
<Suspense> component. In essence,
<Suspense> says, display this loading content while the real content is in a loading state, then show the real data-driven content when ready. Because the UI is not blocked while this is happening and each
<Suspense> happens concurrently, developers have a consistent and simple way to define layouts that are optimized and responsive, even with many data-dependent sections.
/app directory means you by default can use streaming and
<Suspense>. Next’s back-end server implements the API that drives the loading states. The benefits are that loading states can be rendered quickly, hydrated content can be displayed as it becomes available concurrently, and the UI remains responsive while segments are loading.
These benefits are especially pronounced when the network is slow or unreliable, for example with mobile. This new feature improves both user experience and developer experience. Developers will notice that data fetching is more consistent and standard. Adopting best practice is not only simpler, but is the default.
The new loading convention
There is a new
loading.js convention in Next.js 13. It lives in a route directory of
/app and acts like a
<Suspense> for the entire route section. (Under the hood, Next.js actually applies a
<Suspense> boundary.) So, whatever is defined in the folder for
loading.js will show while the actual content is being rendered, with the same benefits of using suspense directly.
You can see this convention in action by opening http://localhost:3000/streaming in our demo app. This will display the
app/streaming/loading.tsx file shown in Listing 6 while the actual content is loaded.
Listing 6. app/streaming/loading.tsx
import SkeletonCard from '@/ui/SkeletonCard'; export default function Loading() return ( <div className="space-y-4"> <div className="grid grid-cols-3 gap-6"> <SkeletonCard isLoading=true /> <SkeletonCard isLoading=true /> //... </div> );
loading.tsx file in Listing 5 shows a grid of SkeletonCard components. A skeleton card is the pulsing media card that holds the places for the real content that is ultimately loaded by the route.
Better data fetching in Next.js 13
The first convention is to load data on the server, which has become simpler because all the components are server components by default. This eliminates the tendency to bounce data requests from the client off the server, when you really only need to directly hit the data store from the server and send the rendered UI to the client. See the Next.js documentation for a longer account of the reasoning behind preferring server-side data fetching.
Data fetching in the
/app directory has to work with streaming and suspense. Components should make their own data requests, instead of parents passing in the data—even if that data is shared between components. The framework itself will avoid redundant requests, and it will ensure only the minimal requests are made and handed to the right components. The fetching API will also cache results for reuse.
All of this makes for a simpler architecture for data fetching that is still optimized. Developers can think less about data fetching performance and just grab data as it’s needed, in the component that needs it.
The new approach means you can use the asynchronous Fetch API that we are familiar with directly in server components. (React and Next extend the API to handle deduping and caching.) You can also define async server components; for example,
export default async function Page(). See the Next.js blog for more about the new fetch API.
The overall effect of all these improvements is a simpler application architecture that still benefits from behind-the-scenes performance optimization.
That’s quite a lot of action in Next.js 13—and there is more that I did not cover. Other new features include updates to the next/image component and a new font-loading system. Overall, Next.js 13 continues the tradition of delivering an all-in-one, React-with-benefits framework that makes it easier to take advantage of a variety of features.
Still, this release is special due to long-term innovations like streaming and server components. When united with Vercel’s infrastructure, Next.js 13 offers considerable ease of deployment and gives us a glimpse of the reactive development experience of the future.
Copyright © 2022 IDG Communications, Inc.