- Published on
Basic guide to optimize the performance of Next.js applications
Step-by-step base of optimizations to improve the performance of your Next.js application, using Core Web Vitals metrics.
- Time to read
- 9min read
I always missed a place where I could centralize the best performance optimization practices that I have already applied in my Next.js projects, even to consult later. I decided to write this post to centralize everything in one place.
This article serves as a basic (and perhaps advanced) guide to optimizations to improve the performance of a Next.js application. Need to improve the score of your Next.js site? Use this article as a base, and optimize step by step the performance of your application.
I will mainly consider the Core Web Vitals metrics, which are the performance metrics that Google uses to evaluate the user experience on a site.
Image Optimization
I would say that image optimization is one of the most important, and one of the simplest to apply with the Image component from Next.js. Just using the component you already get a series of optimizations automatically. If your site uses many images, for example in a blog, or ecommerce, this optimization is essential and will probably give you many points in the Lighthouse score.
The Next.js Image component is very powerful and performs a series of automatic optimizations:
- Responsive images: Automatically creates multiple versions of an image for different screen sizes. This is called responsive images. The browser will download the image that best fits the user's screen size. This is very important for mobile devices, where the screen width is smaller, and the image needs to be smaller as well. This feature considerably reduces the final size of the image.
- Conversion to WebP and AVIF: In the process of creating the image versions, the component automatically converts images to WebP and AVIF, which are more modern and lighter image formats than JPG and PNG. This is done automatically, and the browser will download the image in the format it supports.
- Lazy loading: The Image component automatically lazy loads images, that is, it will only download the image when it is visible on the user's screen. This is also very important for page performance, as it prevents the browser from downloading images that the user will not see on first section.
- Visual Stability: Automatically prevents CLS in advance.
Manually optimizations
For images that use the fill
attribute, always use the sizes
attribute to define the width of your image on different screen sizes. In addition to helping Next to generate more appropriate sizes for the context of use of your image, it will help the browser decide which version of the image to download. If you do not use the sizes
attribute, Next.js will use the default value of 100vw
, which in most cases is a huge waste.
If your image, for example, occupies only 33% of the screen width on desktop, and 50% on mobile, you can define the sizes
attribute as follows: sizes="(min-width: 1024px) 33vw, 50vw"
. Not defining the sizes
attribute in this context would display an image 2x larger than necessary and 3x larger on desktop.
With just this responsive image optimization that Next.js automatically provides, with the addition of the sizes
attribute giving more context of your image display, the size of the image served to the user is drastically reduced. For example, if you have a non-optimized JPG or PNG image of 700kb, correctly using the sizes
attribute, you will notice that the mobile version will easily contain less than 60kb.
It is really impressive the power of the Next.js Image component, even with few additional settings.
Even after being optimized, if your audit tool is accusing LCP on an image from the first fold, you can use the priority
attribute to preload the image.
Unlike images that can be loaded only when the user sees them, using the Lazy Loading feature, there are images that you want to load as soon as possible, for example, images from the first fold of your page. Usually these images affect the LCP of the site. Using the priority
attribute in the Image component, Next.js adds a preload HTML link
tag for the image, causing the browser to load the image in advance.
Font Optimization
Use Next.js next/font
to load your application's fonts. With next/font
, it is possible to load fonts with zero layout shift, and also improve performance with optimized loading.
You can import any font from Google Fonts. The font files are downloaded at build time and served along with your static assets. No request is made to Google in the browser.
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
Code Splitting
During development, it is essential to think about Code Splitting. Code Splitting is the act of splitting the application code into several pieces, and loading only the necessary code for the user's experience.
Dynamic Import
Use Next.js Dynamic Import to dynamically import components only when their use is necessary.
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import Component from '../components/a'
const DynamicComponent = dynamic(() => import('../components/b'))
export default function Example() {
const [showMore, setShowMore] = useState(false)
return (
<div>
{/* Carrega imediatamente */}
<ComponentA />
{/* Carrega sob demanda, apenas quando/se a condição for atendida */}
{showMore && <ComponentB />}
<button onClick={() => setShowMore(!showMore)}>Toggle</button>
</div>
)
}
For a good user experience, it is important to define a fallback for the component that is being dynamically loaded. The fallback is a component that will be displayed while the dynamic component is being loaded. This prevents the user from seeing a blank screen while the component is being loaded, in addition to preventing a possible CLS.
const DynamicComponent = dynamic(() => import('../components/b'), {
loading: () => <p>Loading...</p>,
})
Loading external libraries
External libraries that are used only at specific moments in the application flow, or in specific user interactions, do not need to be loaded in the initial application bundle.
To load them dynamically, use the dynamic import of javascript.
'use client'
import { useState } from 'react'
export default function Example() {
const [showMore, setShowMore] = useState(false)
return (
<div>
<button
onClick={async () => {
const { default: library } = await import('library')
library.init()
setShowMore(true)
}}
>
Show More
</button>
</div>
)
}
Server Components
If your application uses a version equal to or greater than Next.js 13, you can use Server Components. The more Server Components used instead of Client Components, the smaller the final bundle and the less Javascript to be loaded by the user. This will decrease the overall page load time and help with metrics like TBT (Total Blocking Time) and FCP (First Contentful Paint).
Use a development based on Server Components whenever possible, and use Client Components only when necessary. In addition to reducing the size of the final bundle, they increase the initial page load time, since they are not sent to the client and therefore the user does not need to wait for the Javascript to be downloaded.
Optimization of third-party scripts
Use the Next.js Script component to load third-party scripts in an optimized way. Scripts like Google Analytics, Facebook Pixel, Hotjar, among others, affect the performance of your application. They can block page rendering or add extra load time.
For scripts that do not require critical load time or low priority, use the strategy="lazyOnload"
attribute. This loading strategy will load the script only when all page resources have already been loaded, and will probably not impact your final performance score.
import Script from 'next/script'
export default function Home() {
return (
<div>
<h1>My Homepage</h1>
<Script src="https://www.google-analytics.com/analytics.js" strategy="lazyOnload" />
</div>
)
}
OBS: Try to avoid using Google Tag Manager to load scripts and services on your page as much as possible. GTM is a service that loads third-party scripts asynchronously. Always prioritize loading scripts separately directly on your page with specific loading strategies for each one using the Script component.
Bonus
It is also possible to load your script in a different thread from the main thread, using the strategy=worker
attribute. With this, the script will be loaded in a separate thread, and will not block the resources that run on the main thread.
To enable the use of strategy=worker
, you need to enable the nextScriptWorkers
flag in your next.config.js
file:
// next.config.js
module.exports = {
experimental: {
nextScriptWorkers: true,
},
}
It is important to understand that this is an advanced strategy and does not work for all scripts. It is important to know if the service used depends on resources from the main thread, and after implementation perform a battery of tests to test its operation.
Unfortunately this feature is not yet available for the App Router.
Rendering strategies
Use as much as possible static rendering. If your page is a blog post, or an e-commerce product, or any page with non-real-time content, and you have a large user base, there is no reason not to use static rendering.
If your page necessarily needs and uses real-time data, load the page using the streaming strategy. Using streaming to load your page allows the content to be loaded incrementally, it will not block the rendering of the page while all the page data is being loaded.
Display feedback to the user in the parts that are being loaded, such as with Skeletons, while the parts that do not require real-time data are displayed immediately.
Conclusion
The optimizations presented can be done in almost any application. It is important to understand that each project has its own set of challenges and optimizations, and that other optimizations can be created depending on your use case.
However, ensuring that the points in this article have been applied to your project, you will minimally be on the right path to having good performance on your page.
It is important to remember that performance optimization is an ongoing process, and a habit that should be had during development. Performing audits and generating reports constantly are parts of the development process, and essential to continue delivering good performance to your users.
These are the optimizations that I apply in my projects, and that help me to have good performance. I hope they help you too.