Optimizations in Next.js - How to speed up your website?

Blog image - Optymalizacje w Next.js - Jak przyspieszyć swoją stronę?

18/12/2023

12 min

Bartosz Lewandowski

Optimizations in Next.js - How to speed up your website?

18/12/2023

12 min

Bartosz Lewandowski

Blog image - Optymalizacje w Next.js - Jak przyspieszyć swoją stronę?

Table of Contents

  1. Image optimization
  2. Font Optimization
  3. Script Optimization
  4. SEO
  5. Lazy Loading
  6. Third Party libraries
  7. Summary

Also Read: Data Fetching in Next.js - A Beginner’s Guide

In today’s rapidly evolving world of web technologies, resource optimization in web applications is essential for ensuring smooth and efficient operation. In the context of the Next.js framework, this optimization takes on particular importance, especially when it comes to elements such as images, fonts, scripts, SEO, static resources, lazy loading, and integration with third-party services. In this blog post, we will delve into the world of optimizing these key components of a Next.js application. We will discuss how to effectively manage images and fonts to speed up page loading, how to optimize scripts and SEO for better performance, how to efficiently use static resources, and how to smartly implement lazy loading and integrate external services. This approach will not only improve application performance but also offer end-users a significantly better experience when using the site.

Image Optimization

Images constitute a significant part of the load on a typical website, and their proper optimization can greatly impact site performance, especially the Largest Contentful Paint (LCP) metric. The Image component in Next.js extends the standard HTML <img> element with automatic image optimization features, including:

  1. Size optimization: Automatically adjusting image sizes for different devices, using modern formats like WebP and AVIF.
  2. Visual stability: Automatically preventing layout shifts during image loading.
  3. Faster page loads: Images are loaded only when they enter the user's viewport, thanks to native lazy loading in the browser, with optional placeholders.
  4. Resource flexibility: The ability to resize images on demand, even for images stored on remote servers.

How to use Image in Next.js

After importing Image from Next.js, you can specify the image source (both local and remote).

Local images: Import image files (e.g., .jpg, .png, .webp). Next.js will automatically determine the image's width and height, which helps prevent layout shifts during image loading.

import Image from "next/image";
import userPicture from "./me.png";
 
export default function Page() {
	return <Image src={userPicture} alt="Picture of the author" />;
}

Remote images: For remote images, you must manually provide width, height, and optionally blur properties. Next.js does not have access to remote files during the build process, so these additional details are required.

import Image from "next/image";
 
export default function Page() {
	return (
		<Image
			src="<https://images.ctfassets.net/userPicture.png>"
			alt="Picture of the author"
			width={500}
			height={500}
		/>
	);
}

Security

To safely optimize images, define a list of supported URL patterns in next.config.js to prevent unauthorized use. For example, you can allow images only from a specific site images.ctfassets.net.

/** @type {import('next').NextConfig} */ 
const nextConfig = {
	images: {
		remotePatterns: [{ protocol: "https", hostname: "images.ctfassets.net", pathname: "/**" }],
	},
};
module.exports = nextConfig;

Prioritization

Images that will be the LCP element on the page should have the priority attribute set. This allows Next.js to prioritize the image during loading.

<Image src={image} alt="" priority />

Image size

To avoid layout shifts related to images, always specify their size. This can be done in several ways:

  1. Automatically, using static import.
  2. By adding the width and height properties.
  3. By default, using the fill property, which makes the image expand to fill the parent element.

Font optimization

In the context of web development, fonts often play a crucial role not only in aesthetics but also in performance and privacy. The next/font library from Next.js introduces an innovative solution for font management, offering automatic optimization for both standard and custom fonts. More importantly, it eliminates external network requests, significantly improving privacy protection and page loading speed.

Key features of next/font

Automatic self-hosting of each font file: Allows for optimal loading of web fonts without layout shifts, thanks to the use of the CSS size-adjust property.

Easy use of Google Fonts with performance and privacy in mind: CSS and font files are downloaded at build time and self-hosted along with other static resources. No requests are sent to Google by the browser.

Using Google Fonts

To use Google Fonts: Import the selected font from next/font/google as a function. It is recommended to use variable fonts for the best performance and flexibility.

import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], display: "swap" });
 
export default function Layout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en" className={inter.className}>
			      <body>{children}</body>    
		</html>
	);
}

If not using variable font, you must specify the weight:

import { Roboto } from "next/font/google";
const roboto = Roboto({ weight: "400", subsets: ["latin"], display: "swap" });
 
export default function Layout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en" className={roboto.className}>
			      <body>{children}</body>    
		</html>
	);
}

You can specify multiple weights using an array.

Local fonts

To use local fonts: Import next/font/local and specify the path to the local font file. Example of using a local font:

import localFont from "next/font/local";
const myFont = localFont({ src: "./my-font.woff2", display: "swap" });

If you want to use multiple files for one font family, src can be an array.

import localFont from "next/font/local";
const openSans = localFont({
	src: [
		{ path: "./OpenSans-Regular.woff2", weight: "400", style: "normal" },
		{ path: "./OpenSans-Italic.woff2", weight: "400", style: "italic" },
		{ path: "./OpenSans-Bold.woff2", weight: "700", style: "normal" },
		{ path: "./OpenSans-BoldItalic.woff2", weight: "700", style: "italic" },
	],
});

Script optimization

In Next.js, an essential part of optimizing web pages is efficiently managing scripts, especially those from third parties. The next/scripts library allows loading scripts at various levels of the application, ensuring that scripts are loaded efficiently and do not negatively impact page performance.

Scripts in layout.tsx

For all pages: To load a third-party script for all routes, import next/script and include the script directly in the root layout:

import Script from "next/script";
 
export default function RootLayout({ children }) {
	return (
		<html lang="en">
			      <body>{children}</body>
 
			<Script src="<https://example.com/script.js>" />
 
		</html>
	);
}
 

This script will be loaded and executed when any route in the application is accessed.

For a specific group of pages or a single page: To load a third-party script for a specific route, import next/script and include the script directly in the layout component:

import Script from "next/script";
 
export default function DashboardLayout({ children }) {
	return (
		<>
			<section>{children}</section>      
			<Script src="<https://example.com/script.js>" />
 
		</>
	);
}
 

The third-party script is fetched when the user accesses the folder route (e.g., blog/page.js) or any nested route (e.g., blog/settings/page.js). Next.js ensures that the script is loaded only once, even when navigating between multiple routes within the same layout.

Loading strategies

You can customize how scripts are loaded using the strategy property:

  1. beforeInteractive: Load the script before any Next.js code and before page hydration.
  2. afterInteractive (default): Load the script early, but after some page hydration.
  3. lazyOnload: Load the script later, during browser idle time.
  4. worker (experimental): Load the script in a web worker.

Scripts using the worker strategy are executed in a web worker with Partytown, which can improve page performance. To use this feature, enable the nextScriptWorkers flag in next.config.js.

Inline scripts

Inline scripts, which are not loaded from an external file, are also supported by the Script component. They can be written by placing JavaScript in curly braces or using the dangerouslySetInnerHTML property.

Recommendations

Optimal management of third-party scripts in Next.js applications is crucial for maintaining high performance and good user experience. Improper or excessive use of scripts can lead to slow page loading, increased interaction time, and reduced overall user satisfaction. Here are some key recommendations to help you avoid these issues and effectively implement third-party scripts:

  1. Limit the use of third-party scripts: Include third-party scripts only where they are absolutely necessary. Avoid adding unnecessary scripts that may not add value to your application.
  2. Load scripts properly: Use the loading strategies provided by Next.js, such as beforeInteractive, afterInteractive, and lazyOnload, to optimize loading time and script performance. Choose the strategy that best fits the nature of the script and the page's requirements.
  3. Use Web Workers for heavy scripts: For scripts that require intensive computation or may impact the main thread's performance, consider using the worker strategy. However, remember that this is an experimental solution and requires additional configuration.
  4. Monitor performance: Regularly monitor page performance, especially after adding new scripts. Tools like Google PageSpeed Insights can help assess the impact of scripts on performance.
  5. Optimize and minify scripts: Where possible, use minified versions of scripts. Minification reduces file size, which can contribute to faster loading.
  6. Ensure security: Make sure that the scripts you import come from trusted sources. Unsafe or malicious scripts can expose your site and its users to risk.
  7. Test in different environments: Test the operation of scripts in different browsers and devices to ensure they maintain consistency and do not negatively impact user experiences across diverse platforms.
  8. Thorough documentation: Document all third-party scripts used in the application, along with information about their purpose, operation, and any dependencies. This will facilitate management and updates in the future.

Following these recommendations will help maintain a healthy balance between functionality and performance in Next.js applications, while also ensuring positive experiences for users.

SEO

Next.js offers advanced APIs for managing application metadata, which is crucial for improving SEO (Search Engine Optimization) and better web shareability. Metadata, such as meta tags and link elements in the HTML head, are important for search engines and social media, and Next.js makes it easy to manage them effectively.

Two methods for implementing metadata in Next.js

Configurable metadata: You can export a metadata object as a static element or use the generateMetadata function for dynamic metadata generation. This function is placed in the layout.js or page.js file.

File-based metadata: This method involves adding special static or dynamically created files to specific path segments in the application. In both cases, Next.js automatically generates the appropriate <head> elements for pages. You can also create dynamic OG (Open Graph) images using the ImageResponse constructor.

Static metadata

To define static metadata, export the Metadata object from the layout.js or static page.js file.

import type { Metadata } from "next";
export const metadata: Metadata = { title: "...", description: "..." };
export default function Page() {}

Dynamic metadata

Use the generateMetadata function to fetch metadata requiring dynamic values.

import type { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata({ params, searchParams }, parent): Promise<Metadata> {}
export default function Page({ params, searchParams }) {}

Notes

  1. Static and dynamic metadata via generateMetadata are supported only in Server Components.
  2. Fetch requests are automatically memoized for the same data in different contexts.
  3. Next.js waits for data fetching in generateMetadata to complete before sending the UI to the client.

File-based metadata

Special files available for metadata include favicon.ico, apple-icon.jpg, icon.jpg, opengraph-image.jpg, twitter-image.jpg, robots.txt, sitemap.xml. They can be used for static metadata or generated.

JSON-LD

JSON-LD (JavaScript Object Notation for Linked Data) is a structured data format that plays a crucial role in SEO (Search Engine Optimization). It is used to convey information about the structure and content of a web page in a format that search engines, such as Google, can easily interpret. This allows for more efficient indexing and presentation of content in search results.Application of JSON-LD:

  1. Improving SEO: JSON-LD allows for precise communication of information about page content to search engines, which can significantly improve its visibility and ranking in search results.
  2. Rich snippets: Using JSON-LD enables the creation of "rich snippets," which enhance displayed search results with additional information, such as ratings, prices, product availability, etc.
  3. Structured content descriptions: You can describe various types of content, such as articles, events, products, people, organizations, recipes, and more.

Implementing JSON-LD in Next.js: In Next.js, JSON-LD is typically implemented by placing appropriate JSON-LD scripts in the <script> tag in the component representing the page structure (such as layout.js or page.js). For example:

export default function ProductPage({ product }) {
	const jsonLd = {
		"@context": "<https://schema.org>",
		"@type": "Product",
		name: product.name,
		image: product.image,
		description: product.description,
	};
	return (
		<section>
 
			<script
				type="application/ld+json"
				dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
			/>
			      {}    
		</section>
	);
}

Important aspects of JSON-LD:

  1. Data typing: JSON-LD allows for precise specification of data types (e.g., Product, Article, Event), which helps search engines understand the content's context.
  2. Validation: Tools for validating JSON-LD code, such as Google Rich Results Test, help verify that data is correctly formatted and understandable by search engines.
  3. Compatibility and flexibility: JSON-LD is compatible with various data formats and can be easily integrated with different web technologies, including Next.js applications.

Lazy Loading

Lazy loading in Next.js is a technique that helps improve the initial loading performance of an application by reducing the amount of JavaScript needed to render a route. It allows client components and imported libraries to be loaded only when they are actually needed. For example, you can delay loading a modal until the user clicks to open it.

Methods for implementing Lazy Loading

Using dynamic imports with next/dynamic: next/dynamic is a composite of React.lazy() and Suspense, which behaves similarly in both app and page directories, allowing for gradual migration.

Examples:

  1. Importing client components:
import dynamic from 'next/dynamic'const ComponentA = dynamic(() => import('../components/A'))
  1. Disabling SSR for a client component:
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
  1. Importing server components:
const ServerComponent = dynamic(() => import('../components/ServerComponent'))
  1. Using React.lazy() with Suspense: An alternative method is using React.lazy() in conjunction with Suspense.

Loading external libraries:

import { useState } from "react";
const books = [
	"The Great Gatsby",
	"To Kill a Mockingbird",
	"1984",
	"Pride and Prejudice",
	"The Catcher in the Rye",
];
export default function BookSearchPage() {
	const [searchResults, setSearchResults] = useState([]);
	return (
		<div>
 
			<input
				type="text"
				placeholder="Search for a book"
				onChange={async (e) => {
					const searchQuery = e.currentTarget.value;
					if (searchQuery.length > 0) {
						const _ = (await import("lodash")).default;
						const results = _.filter(books, (book) =>
							_.includes(book.toLowerCase(), searchQuery.toLowerCase()),
						);
						setSearchResults(results);
					} else {
						setSearchResults([]);
					}
				}}
			/>
 
			<ul>
 
				{searchResults.map((book, index) => (
					<li key={index}>{book}</li>
				))}
 
			</ul>
 
		</div>
	);
}

Adding a custom component:

import dynamic from "next/dynamic";
const Loading = dynamic(() => import("../components/Loading"), {
	loading: () => <p>Loading...</p>,
});

Key aspects

  1. Performance Optimization: Lazy loading reduces the amount of JavaScript required to load upon initial entry to the page, speeding up load times.
  2. On-demand Loading: Components and libraries are loaded only when needed, which is especially useful for interactive elements like dialogs or modals.
  3. Support for Server and Client Components: This technique primarily concerns client components but can also be applied to server components.
  4. Flexibility in Implementation: The ability to choose between next/dynamic and React.lazy() gives developers flexibility in applying lazy loading according to their application's needs.

Third Party libraries

The @next/third-parties library in Next.js provides a set of components and tools that facilitate and optimize the loading of popular third-party libraries in Next.js applications, enhancing performance and developer experience. Available integrations in the @next/third-parties library in Next.js include:

  1. Google Tag Manager: Allows integration with Google Tag Manager for managing tags on the site.
  2. Google Maps Embed: Provides easy embedding of Google Maps directly on the page.
  3. YouTube Embed: Allows embedding YouTube videos on the page using an optimized loading method.

Summary

In summary, in Next.js, effective management of images, fonts, scripts, as well as optimization of SEO, lazy loading, and integration with external libraries is crucial. Using the Image component ensures automatic image optimization, reducing page load time. Font management, including self-hosting, enhances performance and privacy. Efficient script loading, including third-party scripts, contributes to better page performance. SEO optimization and the use of JSON-LD are key for better search engine ranking. Lazy loading improves initial load performance, and integration with external libraries facilitates working with popular tools.

Mastered the topic? Great! In that case, I invite you to explore the next important topic in the following post - "Styling in Next.js - A Beginner’s Guide," which will surely enrich your knowledge and skills in creating attractive, responsive user interfaces.

Frequently Asked Questions

  1. How to optimize images in Next.js? Use the Image component for automatic image optimization. It adjusts image sizes for different devices and supports modern formats like WebP, ensuring faster page loading.
  2. Can I use custom fonts in Next.js? Yes, with the help of the next/font library, you can easily integrate custom fonts, both local and from Google Fonts, with automated self-hosting and performance optimization.
  3. How to manage third-party scripts in Next.js? Use next/script for efficient loading of third-party scripts. You can control when and how scripts are loaded using different loading strategies, such as beforeInteractive and lazyOnload.
  4. How to improve SEO in Next.js? Take advantage of Next.js's advanced features for managing metadata and SEO tags. You can use static or dynamic metadata and implement JSON-LD to enhance the structural description of page content.
  5. What is lazy loading in Next.js and how to use it? Lazy loading allows components and libraries to be loaded only when needed, increasing initial load performance. It can be implemented using next/dynamic or a combination of React.lazy() with Suspense.
  6. How to integrate third-party libraries in Next.js? Utilize @next/third-parties, which offers a set of components that facilitate the integration of popular third-party libraries like Google Maps or YouTube, improving performance and user experience.
  7. What to do to prevent images from causing layout shifts in Next.js? Always specify image sizes using the Image component, using width and height properties or the fill option for flexible fitting of images to the container. This will prevent layout shifts during image loading.
Bartosz Lewandowski

Bartosz Lewandowski

CEO

Newsletter