Ben Schwarz
December 2, 2021
Illustrated by
Next.js framework is known for quick navigations and excellent developer experience. The creators and maintainers of Next.js invest time in tooling and options that help make it fast.
Having an optimised framework is a great starting point, but as your sites and apps grow, you’ll need to make the most of your frameworks’ features to offer your users a superior level of experience.
In this post, we’ll describe how your team can get the most out of Next.js when it comes to performance.
One of the most effective ways to keep Next.js client-side bundles small is to utilise dynamic imports, a technique also known as code-splitting.
Code-splitting allows you to lazily-load application code or dependencies, as required by each user journey. In practice, this means that JavaScript bundles are broken into smaller chunks (many small JS files).
This benefits users in numerous ways:
In the following example, we significantly reduced first-load JavaScript by dynamically loading SVG logo assets, keeping them from appearing in the main JavaScript bundle:
1// components/CompanyLogos/index.js23// Before4import TastyLogo from 'images/logos/logo-tasty.svg'56// After7import dynamic from 'next/dynamic'89const TastyLogo = dynamic(() => import('images/logos/logo-tasty.svg'))
After switching SVG logos to be loaded dynamically, we saw a bundle reduction of 82KB. Now each SVG asset is inlined to the page without appearing in JavaScript bundles:
For a complete list of options for dynamic loading, see the official Next.js documentation.
The reality for many developers is there’s often no choice but to add third party scripts for analytics, tracking or customer communication tools, simply because there’s a business requirement for it.
Adding heavy scripts to a page is a sure-fire way to slow down user experiences, but that doesn’t mean we don’t have control over how these resources load! By using next/script, you can specify when the browser should fetch selected scripts:
1// pages/index.js2import Script from 'next/script'34export default function Home() {5 return (6 <>7 // page components go here8 <Script9 src="https://www.google-analytics.com/analytics.js"10 strategy="lazyOnload"11 />12 </>13 )14}
In the above example, Google Analytics will be fetched after all other page resources once the browser has become idle. It’s crucial to load critical page resources first without making visitors wait for third-party add-ons and tracking scripts.
next/script comes with three loading strategies:
If you’d like a script to have minimal impact on your page, use lazyOnload.
Find out more about next/script in the official Next.js documentation.
Next’s <Image /> is a powerful component that trivialises the generation of responsive images, optimised using the latest compression algorithms.
Here are some of the highlights of <Image />:
You can enable AVIF and WebP by adding configuration to next.config.js:
1// ./next.config.js2{3 images: {4 formats: ['image/avif', 'image/webp']5 }6}
By offering AVIF images to supported clients, we reduced large detailed blog illustrations from 300Kb to around 100Kb for large desktop displays.
Next.js’ Image component provides the power and flexibility that most pages require and is a solid upgrade on base functionality provided by native <img /> elements.
We recommend you familiarise yourself with its many configurable options so that you can provide users with fast-loading images every time.
Next.js currently comes with three methods of rending pages. It’s valuable to familiarise yourself with the options available and their inherent strengths and weaknesses.
While these rendering methods are built directly into Next.js, their concepts can be applied to most tech stacks, though the level of work required to achieve them will vary greatly.
Next.js offers a path of low resistance to get good results, and it’s part of why we love the framework. Below, we’ve described each render mode and our recommendations to get the best bang for the buck.
Server rendered pages are generated for each request that comes into your server. You’ll use SSR for highly dynamic content that needs to stay up to date.
If the content is considered “fresh” for a given time, you can add Cache-Control headers to instruct Vercel/CDNs to cache the resulting HTML for a given period:
1export async function getServerSideProps({ query, res }) {2 // Cache the content of this page for 12 hrs3 // After 12 hrs, the content will continue to be served4 // for a grace period of 60 seconds as new data is fetched5 // Then, the CDN will store a fresh copy for the next user :-)6 res.setHeader(7 'Cache-Control',8 'public, s-maxage=43200, stale-while-revalidate=60'9 )1011 const { title, body, imageUrl } = await getPageDataFromCMS('home')1213 return {14 props: {15 generatedAt: Date.new().toString(),16 title,17 body,18 imageUrl19 }20 }21}
In the example above, the page will fetch the dynamic data from a CMS when a user requests it.
If you’re using Vercel or an intermediate CDN/reverse proxy server, the resulting HTML response will be cached for 43200 seconds (12 hours). When the page cache expires, the CDN will continue to serve it for another 60 seconds while it is re-generated by the server.
By partnering SSR pages with well-thought-out caching strategies, you’ll be able to offer fast dynamic content to all viewers.
Incremental static regeneration is excellent for blogs and publications. If you’ve got content that can be rendered once (per deployment), and it won’t change after that, ISR might be your new best friend.
Much of the content we read online is written to attract two distinct audiences: people and SEO spiders. In both cases, content must arrive as quickly as possible.
If you statically build pages during deployment, you’ll spend time waiting for the pages to build.
An ISR page differs by waiting until the browser requests the page before generating it. That process looks like this:
By using this approach in a content-heavy blog (with many hundreds or even thousands of articles), you’re able to circumvent build wait time significantly while still offering viewers sub-second page renders.
Statically rendered pages are generated when your site is built. If you don’t have a lot of content, and it doesn’t change between deployments, statically rendered pages will always offer the overall fastest load (without concerning yourself with headers or page caching).
Statically rendered pages are the default Next.js rending method when getServerSideProps and getInitialProps aren’t in use.
To recap rendering modes options:
In December 2020, the React team announced React Server Components, which are currently available as a React 18 Next.js beta feature. While Next.js already offers SSR, ISR and Static rendering, a JavaScript bundle is still required in all cases, even when content is not dynamic.
React Server Components will render components entirely server-side, making it possible to have a zero-KB client-side bundle.
With Server Components, you’re able to opt-in which parts of your application are rendered on the server and when client-side code is required. In Next.js, a server component will be denoted by filename, e.g., page-name.server.js, whereas a client-side component will use page-name.client.js.
In the following example, we can fetch content from a CMS, import a date-time utility (date-fn) and render markdown. The resulting client-side JavaScript from this page will only include the ShareMenu, which is dynamically loaded. React will resolve <Suspense> boundaries client-side. That way, you can first deliver server-rendered content, then enhance the page with client-side components:
1// pages/[post].server.js2import dynamic from 'next/dynamic'3import React, { Suspense } from 'react'4import format from 'date-fns/format'56import Markdown from 'components/Markdown.server.js'78const ShareMenu = dynamic(() => import('components/ShareMenu.client.js'))910import { getPostContent } from 'data/cms'1112export default function Post({ slug }) {13 const post = getPostContent({ slug: slug })1415 return (16 <article>17 <header>18 <h1>{post.title}</h1>19 <p>Published: {format(post.publishedAt, "h:mma 'on' MMM do yyyy")}</p>20 </header>2122 <Suspense>23 <ShareMenu />24 </Suspense>2526 <Markdown source={post.body} />27 </article>28 )29}
All JavaScript based frameworks require some form of JavaScript compilation. Under the hood, Next.js uses Webpack and Babel to compile JavaScript bundles. Babel has served us well up until now, but its speed is limited by using a JavaScript runtime.
Enter SWC, a new Rust-lang based compiler for JavaScript. Next.js 12 announced SWC as a new experimental JS bundler. SWC offers far superior compilation speeds, meaning that live-reloads, next build and deployments will all be faster.
While SWC actively offers website viewers a superior experience, developers can work more quickly and move faster. Let’s use that gained time to enhance user experiences as best we can!
By learning how to use and configure Next.js effectively, you’ll be able to offer customers better, faster web experiences with less effort. We’ll be sure to keep this article up to date with Next’s best practices. If you have other recommendations, let us know!
Join thousands of subscribers keeping up-to-date with web performance news and advice.
Addy Osmani
Engineering Manager at Google Chrome