Next.js

Next.js Review and Features

Written by Thom Krupa, Melvin Kosisochukwu

Last update: 6/11/2024

Next.js 1.0 - the beginning

It was fall of 2016 when I discovered the first version of Next.js. I created a simple file that looked like this:

// pages/index.js
export default () => <div>Hello world!</div>

I executed in the terminal and the magic happened. Two lines of code, one command later, and my new hello world website went live. It was hard to compare that experience to developing custom WordPress themes. Next.js and Now (currently Vercel) felt incredibly easy. Almost like cheating.

A few months later, I shipped my first project using Next.js and the WordPress API. The project had some serious performance issues. Fetching data on every request from many API endpoints wasn't the best idea. I probably should export those pages to static ones, right? But it wasn't trivial in the first versions of Next.js.

Fast-forward to Next.js 9.3

Next.js evolved to support static site generation. Vercel evolved to support serverless.

Version 9.3 introduced three new data fetching methods:

  • to fetch data at build time, this can be content for a single post
  • to specify a collection of dynamic routes, for example, a list of blog posts
  • to fetch data on every request.

Below is a simple example of static generation:

// pages/posts/[slug].js
// [slug] filename means it's a dynamic parameter
// it will be passed to getStaticProps `params` object
const BlogPost = ({ data }) => <Post content={data} />

// executed only at build time
export async function getStaticPaths() {
  const response = await fetch('https://api.domain/posts')
  const data = await response.json()

  // all paths we want to pre-render
  const paths = posts.map((post) => ({
    params: { slug: post.slug }
  }))

  return { paths }
}

// executed only at build time
export async function getStaticProps({ params }) {
  const response = await fetch(`https://api.domain/posts/${params.slug}`)
  const data = await response.json()

  return { props: { data, fallback: false } }
}

export default BlogPost

The code above fetches from the API all blog posts at build time and creates array with for each of them. Data for each single blog post is fetched in based on the param.

If you are familiar with Gatsby, you can notice that is a bit like in . But I think Next approach is easier to understand. The difference is that you don't need to specify a path to a template and passing in . In Next, everything is in the same file. In this case, value is accessible through a query parameter. To learn more check the Migrating from Gatsby article.

If you add a new post you need to re-build the project unless you change to . If there is no existing HTML file on the CDN, Next.js will try to fetch content on the client and cache it. Fallback is very useful if you have a large collection of posts that updates frequently.

Image component

Next.js 10, among other things, has introduced a new, built-in image component and optimization. From now on you don't have to worry about shipping large images for mobile devices. Next handles resizing and generates a modern WebP image format that is approximately 30% smaller than JPG. If your website has a lot of images this can greatly reduce bandwidth and client-side performance.

To use component import it from :


const Hero = () => (
  <section>
    <Image src="/assets/cute-hero.png" width={500} height={500} />
    <h1>Hello world!</h1>
  </section>
)

export default Hero

Next.js' approach doesn't affect build time at all. All optimizations are done at request time. Currently, Next supports 4 cloud providers: Vercel, Imgix, Cloudinary, and Akamai.

Next.js 13 - what’s new?

The following are some of the new features that were introduced in Next.js 13:

  • A new Image component - the new Image component makes it easier to display images without layout shift and optimize files on-demand for increased performance.
  • A new font system - the new Font system automatically optimizes fonts, including custom fonts. It also removes external network requests for improved privacy and performance.
  • A new App Router - the new App Router is a more flexible and powerful way to manage routing in your Next.js application.
  • Improved TypeScript support - TypeScript support has been improved in Next.js 13, with better type checking and more efficient compilation.

The app directory

The app directory in Next.js is a new feature introduced in version 13. It allows you to create a separate directory for your application code, which can help improve the performance and scalability of your application.

The app directory is located at pages/app. This directory contains all of the files that make up your application, such as components, pages, and layouts.

When you use the app directory, Next.js will automatically pre-render all pages at build time. It can improve the performance of your application for users who visit your site for the first time.

The app directory also helps to improve the scalability of your application. Separating your application code from pages can make it easier to scale your application horizontally.

What do you stand to gain by using the app directory in Next.js?

  • Improved performance - pre-rendering all of your pages at build time can improve the performance of your application for users who visit your site for the first time.
  • Improved scalability - separating application code from pages can facilitate horizontal scaling of your application.
  • Easier to management - the app directory can help make your application code easier to manage as the entire application code is located in a single directory.

React server components

React Server Components (RSCs) are a new feature that allows you to render React components on the server. This can be used to improve the performance of your application, as the server can render the components before they are sent to the client.

RSCs are similar to regular React components but there are a few differences. First, RSCs must be exported as a function. Second, RSCs cannot access any state or props that are not defined in the function's parameters.

Here is an example of a React Server Component:

export default function MyServerComponent(title) {
  return (
    <div>
      <h1>{title}</h1>
    </div>
  );
}

This component takes a title prop as input and renders a heading with the title.

To use an RSC, you need to wrap it in a StaticRoute component. The StaticRoute component takes a path and an RSC as input.

Here is an example of how to use an RSC in Next.js:

import MyServerComponent from './MyServerComponent';

export default function Home() {
  return (
    <StaticRoute path="/" component={MyServerComponent} />
  );
}

This code will render the MyServerComponent component when the user visits the / path.

Layouts

In Next.js, a layout is a React component shared across multiple pages. Layouts can be used to share common UI elements, such as a header, footer, or sidebar. They can also be used to apply global styles to your application.

There are two ways to define a layout in Next.js:

Global layout

You can define a global layout by exporting a React component from a layout.js file. This layout will be applied to all pages in your application.

Here is an example of a global layout in Next.js:

export default function Layout() {
  return (
    <div>
      <header>
        <h1>My Site</h1>
      </header>
      <main>{children}</main>
      <footer>
        <p>Copyright (c) 2023</p>
      </footer>
    </div>
  );
}

This layout defines a header, main, and footer section. The children prop is a placeholder for the content of the page.

Per-page layout

You can also define a per-page layout by adding a getLayout prop to your page component. This prop should return a React component that will be used as the layout for that page.

Here is an example of a per-page layout in Next.js:

export default function Page() {
  const layout = getLayout();
  return (
    <layout>
      <h1>This is my page</h1>
    </layout>
  );
}

This component defines a heading with "This is my page" text. The getLayout prop returns the global layout which is then used to wrap the content of the page.

Some of the benefits of using layouts in Next.js:

  • Consistency - layouts can help improve the consistency of your application by ensuring that all pages have the same basic structure.
  • Readability - layouts can make your application easier to read by grouping common UI elements together.
  • Performance - layouts can help improve the performance of your application by reducing the amount of code that needs to be rendered on each page.

Streaming

Streaming is a feature in Next.js that allows you to send data to the client as it becomes available. It improves your application's performance by reducing the time the client needs to wait for the entire page to load.

Streaming is supported in the latest version of Next.js, which is 13.4. To use streaming, you need thegetInitialProps prop on your page component. The getInitialProps prop is a function called when the page is first requested. In this function, you can use the fetch API to fetch data from an API server.

Once you have fetched the data, you can use the stream method to send it to the client. The stream method takes a function as an argument. This function will be called repeatedly with chunks of data as they become available.

Here is an example of how to use streaming in Next.js:

import { Poppins } from "next/font/google";
import { Suspense } from "react";

interface ITodo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const poppins = Poppins({ weight: "600", subsets: ["devanagari"] });


const TodoItem = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const data = await res.json() as ITodo;

  return <div>{data?.title}</div>;
};

export default function Posts() {
  return (
    <main className="flex min-h-screen flex-col gap-4 items-center p-24">
      <h1 className={poppins.className}>Hello World</h1>
      <section>
        <Suspense fallback={<p>Loading todo...</p>}>
          <TodoItem />
        </Suspense>
      </section>
    </main>
  );
}

This code will fetch data from the API server and stream it to the client. The client will then render the data as it becomes available. The Suspense component is used to wrap an asynchronous component and display a fallback spinner or skeleton until the asynchronous process is completed. Then it will swap the fallback component for the component wrapped in Suspense.

Streaming can be a great way to improve the performance of your Next.js application. It can also be used to create more interactive and engaging user experiences.

Here is what you stand to gain by using streaming in Next.js:

  • Improved performance - streaming can improve your application's performance by reducing the time the client needs to wait for the entire page to load.
  • Interactive user experiences -streaming can be used to create more interactive and engaging user experience by loading priority content as soon as it is ready and sections of pages that do not require data to be fetched.
  • Reduced bandwidth usage - streaming can reduce bandwidth usage by only sending data to the client as it becomes available.

@next/font

The @next/font package is a Next.js module that allows you to optimize your fonts, including custom fonts. It also removes external network requests for improved privacy and performance.

The @next/font package is built into Next.js 13.2, so you don't need to install it separately. It allows you to use Google Fonts pre-built or custom local fonts in your file directory.

import { Poppins } from "next/font/google";

const poppins = Poppins({ weight: "600", subsets: ["devanagari"] });

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <h1 className={poppins.className}>Hello World</h1>
    </main>
  );
}

The code block above enables using the Poppins font from Google Fonts. We can also use local fonts:

import localFont from "next/font/local";

const font = localFont({
  src: "/fonts/poppins-v15-latin-regular.woff2",
});

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <h1 className={font.className}>Hello World</h1>
    </main>
  );
}

The local fonts requires an option object for the font properties, as shown in the code block above. The src key should be assigned the file path of your font located in your project directory.

The @next/font package also includes other features, such as:

  • Automatic fallback fonts - if a user's browser doesn't have the specified font, the @next/font package will automatically fallback to a default font.
  • Self-hosting - the @next/font package can self-host your fonts, which means that you don't need to upload them to a CDN. This can improve performance and privacy.
  • Fonts optimization - the @next/font package can optimize your fonts for performance. It includes minifying the font files and removing unused glyphs.

Improved next/link and next/image

The next/link and next/image components in Next.js have been improved in a number of ways in recent versions. These improvements include:

  • better performance - the next/link and next/image components now use native browser features for lazy loading and image optimization, which can improve the performance of your application;
  • more flexibility - the next/link and next/image components now support a wider range of props, which gives you more flexibility in how you use them;
  • better accessibility - the next/link and next/image components now have better accessibility support, which makes your Next.js application more accessible to users with disabilities;
  • the next/link does not require you to nest an anchor (<a>) tag within the Link component, creating a better and improved development process;
  • the Next.js 13 update comes with an improved and easy to style Image component.

Differences between Next.js 13 and the previous version

There are several differences in the handling of certain elements in Next.js 13 compared to its previous version, 12. Apart from the recent enhancement and addition to the file structure with the app directory, changes have also been made to Layouts and the handling of routing in Next.js13. Moreover, version 13 provides an improved method for managing fonts, a topic we've covered in previous sections.

Here is a table that summarizes the key differences between Next.js 13 and the previous version:

[@portabletext/react] Unknown block type "table", specify a component for it in the `components.types` prop

Trade-offs and advantages

The latest Next.js Version 13 offers an enhanced upgrade from its predecessor, boasting exciting new features and substantial performance improvements. However, with these developments come certain trade-offs compared to the previous version. In the following section, we will delve into a detailed review of these advantages, improvements, and the associated trade-offs.

Here are some of the trade-offs and advantages of using Next.js 13:

Advantages

  • The move to Turbopack compiler as a replacement for Webpack shows a drastic reduction in build time, improving the development experience.
  • Next.js 13 is compatible with React 18 and all its new features (Server-side Component, React Streaming, etc.).
  • Improved image optimization and styling - version 13 has an improved Image component with an enhanced optimization service and improved styling for the component.
  • Improved User Experience - Next.js version 13 enhances the user experience by implementing partial rendering of the available webpage components concurrently while data for other components is being fetched. This technique eliminates the need to wait until all data is fetched before starting the rendering process.

Trade-offs

  • With version 13 being relatively new and the app directory just out of beta in 13.4, there is a high chance of bugs and application breaking.
  • Learning curve - working with the new app directory and some of the new features might pose a learning curve.
  • Since the version is relatively new, some community libraries might not be updated to support the features in Next.js 13.
  • The update to version 13 might lead to the deprecation of legacy code or older features.

File Structure

Next.js is zero-config from the very beginning.

nextjs13
 ├── app
 │ ├── favicon.ico
 │ ├── globals.css
 │ ├── layout.tsx
 │ └── page.tsx
 ├── public
 │ ├── next.svg
 │ └── vercel.svg
 ├── .eslintrc.json
 ├── .gitignore
 ├── README.md
 ├── next-env.d.ts
 ├── next.config.js
 ├── package-lock.json
 ├── package.json
 ├── postcss.config.js
 ├── tailwind.config.js
 └── tsconfig.json

Every file in directory is a lambda function.

Ecosystem

Next.js has a great, growing community. You can read some interesting conversations and discuss RFC on Github. Vercel's team is very clear about the framework direction and open to community suggestions.

There is a lot of examples showing integration with different tools like Headless CMS, CSS-in-JS, or auth.

Showcase

[@portabletext/react] Unknown block type "showcase", specify a component for it in the `components.types` prop

How to get started?

The easiest and recommended way to start a new Next.js project is to use:

npx create-next-app
# or
yarn create next-app

If you want to learn more, I highly recommend the official Learn Next tutorial.

It helps to understand how to navigate between pages, add static assets, and fetch data. Once completed you will be ready to build your first Next.js application!

Deploying Next.js

The recommended platform is, of course, Vercel. Its CDN is designed at the edge to support features like incremental static generation, where pages first are populated into the durable store (S3), then the pathname is purged to ensure users are able to see the most recent content.

Preview mode works seamlessly on the Vercel platform, as well as the fallback feature. You can connect your repository and everything works out of the box with no additional config needed.

If for some reason you don't want to use Vercel, it is possible to deploy Next.js on every modern hosting platform like Netlify, Render, AWS, DigitalOcean, and so on. Netlify maintains a special next-on-netlify package that enables the server-side rendering of pages. Incremental static regeneration, fallback, and preview don't work exactly like on Vercel, so it's not a 1-to-1 replacement.

Conclusion

With Next.js you can build a full spectrum of websites and apps. From simple marketing pages and blogs to eCommerce and PWA with cookie-based authentication.

It's great for projects that require flexibility in how you build specific parts of the website. First-world static site generator with capabilities of dynamic enhancement. Hybrid mode is what really shines. Does it mean it's perfect for everything? Not at all. If you don't need React and don't plan to update the project frequently it might be overkill.

[@portabletext/react] Unknown block type "prosCons", specify a component for it in the `components.types` prop