Recently finished the official tutorial (Chinese official website), as an introductory tutorial, the quality is quite good, and the initial code template of the tutorial is also very easy. By following the steps, one can become familiar with some design concepts and development patterns in Next.js, ultimately achieving and deploying a dashboard project on Vercel. Check out my Demo 👉 Entrance
Next.js has a lot of concepts, and the official website provides very detailed explanations. The first-hand materials for learning new technologies always come from official documentation. This article does not do translation work but instead summarizes features related to performance/experience optimization. Now, let's get into the main content.
What is Next.js#
Next.js is a front-end application development framework based on React, aimed at developing high-quality and high-performance front-end applications. It is very powerful and, like its official website, very cool:
- File system-based routing
- Built-in support for UX & CWV performance optimization-related components
- Dynamic HTML streaming rendering
- Latest React features
- Supports various CSS modes, CSS modules, TailwindCSS, and various CSS variants like Sass and Less
- C/S/ISR, server components, server data validation, fetching
Coincidentally, at the Next.js 2024 Developer Summit, I got a virtual entry pass 😬:
The "built-in performance optimization-related components" mentioned above mainly include the following:
Image - LCP & CLS#
According to the Web Almanac, images account for a much larger proportion of static resources on the internet compared to HTML, CSS, JavaScript, and Font resources. Moreover, images often determine a website's LCP performance metric. Therefore, Next.js extends the <img>
tag and provides the next/image component, which implements the following optimizations:
- Size optimization: Provides modern browser image format support for different devices: WebP/AVIF
- CLS optimization: Predefined width and height placeholders during image loading to ensure visual stability
- Accelerates initial page load speed: Lazy loading of images, blurred images
- Size adaptive flexible adjustment: Displays responsive images for devices
Usage:
// Local image
import Image from 'next/image'
import profilePic from './me.png'
export default function Page() {
return (
<Image
src={profilePic}
alt="Picture of the author"
// width={500} automatically provided
// height={500} automatically provided
// blurDataURL="data:..." automatically provided
// placeholder="blur" // Optional blur-up while loading
priority // fetchPriority="high" increases loading priority
/>
)
}
Note: await import
or require
is not supported; static import
is used for analyzing image information at build time.
// Network image
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://s3.amazonaws.com/my-bucket/profile.png"
alt="Picture of the author"
width={500} // manual set
height={500} // manual set
/>
)
}
Note: Width and height need to be set manually.
Video#
Best practices:
- Fallback Content: Display fallback content when the video tag is not supported
- Provide subtitles and captions: Accessibility support
- Compatibility video controls: Support keyboard operation video controls (controls) related functions
Benefits of self-hosted video resources:
- Fully controllable, not subject to third-party restrictions
- Freedom to choose storage solutions: Choose high-performance, elastic CDN for storage
- Balance storage capacity and bandwidth
Next.js provides the solution: @vercel/blob
Font - CLS#
In Next.js, when referencing fonts, the font resources are downloaded and hosted on its own server during the build phase, eliminating the need for additional network requests to Google to download fonts. This is beneficial for privacy protection and performance improvement. The optimization capabilities provided by the component include:
- Supports using all Google Fonts, auto subset to reduce font size, loading only part of the character set
- Font resources are also deployed under the same main domain
- Font resources do not occupy additional requests
- When using multiple fonts, they can be loaded on demand, such as loading only on specific pages, within layout scope, and globally
How to use#
- Functional tool
// app/fonts.ts
import { Inter, Roboto_Mono } from 'next/font/google'
export const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
export const roboto_mono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
})
// app/layout.tsx
import { inter } from './fonts'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>
<div>{children}</div>
</body>
</html>
)
}
// app/page.tsx
import { roboto_mono } from './fonts'
export default function Page() {
return (
<>
<h1 className={roboto_mono.className}>My page</h1>
</>
)
}
- CSS Variables style:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const roboto_mono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={`${inter.variable} ${roboto_mono.variable}`}>
<body>{children}</body>
</html>
)
}
// app/global.css
html {
font-family: var(--font-inter);
}
h1 {
font-family: var(--font-roboto-mono);
}
- with Tailwind CSS:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
plugins: [],
}
Metadata - SEO#
Metadata provides the following functions:
- Easily set the website's TDK (title, description, keywords)
- Open Graph settings for social platforms (Facebook, Twitter) sharing information such as title, description, image, author, time, etc.
- Robots control how search engine crawlers handle pages, whether to index, track, cache
- Supports asynchronous generation of MetaData
How to use#
- Static configuration
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '...',
description: '...',
}
export default function Page() {}
- Dynamic configuration
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// read route params
const id = params.id
// fetch data
const product = await fetch(`https://.../${id}`).then((res) => res.json())
// optionally access and extend (rather than replace) parent metadata
const previousImages = (await parent).openGraph?.images || []
return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
}
}
export default function Page({ params, searchParams }: Props) {}
Note: Metadata generated through asynchronous generateMetadata
is only supported in Server Components. Next.js will wait for the asynchronous execution to finish before streaming the UI back, ensuring that the first segment of the stream response contains the correct <head>
tag.
Scripts - LCP & INP#
The optimization capabilities provided by the component include:
- Third-party dependencies can be shared between multiple route pages, loaded only once
- Dependencies support loading by Layout/Page granularity, loaded on demand
- Supports configuring various loading strategies:
beforeInteractive
,afterInteractive
,lazyLoad
,worker
(using partytown to load third-party dependencies in a Web worker)
How to use#
- Layout/page scripts, load third-party dependencies only under specific Layout/Page to reduce their impact on performance.
// app/dashboard/layout.tsx
import Script from 'next/script'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<section>{children}</section>
<Script src="https://example.com/script.js" />
</>
)
}
- Adjust loading strategies
beforeInteractive
: Before page hydrationafterInteractive
: After page hydrationlazyLoad
: Load during browser idle timeworker
(experimental): Enable Web worker loading, non-CRP JS should be executed in the Worker, yielding to the main thread:
- Supports inline scripts
id
prop required for Next.js to track and optimize Scripts.
<Script id="show-banner">
{`document.getElementById('banner').classList.remove('hidden')`}
</Script>
// OR
<Script
id="show-banner"
dangerouslySetInnerHTML={{
__html: `document.getElementById('banner').classList.remove('hidden')`,
}}
/>
- Supports three callbacks
onLoad
: Script loading completedonReady
: After script loading is completed, every time the component mountsonError
: Script loading failed
Package Bundling - LCP & INP#
Product Analysis#
Similar to webpack-bundle-analyzer, Next.js also provides @next/bundle-analyzer
for product analysis, used as follows:
// pnpm add @next/bundle-analyzer -D
/** @type {import('next').NextConfig} */
const nextConfig = {}
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(nextConfig)
// ANALYZE=true pnpm build
Optimized Imports#
List of third-party dependencies optimized by Next.js, imported on demand, not in full.
External#
List of third-party dependencies externalized by Next.js, similar to webpack externals, specify dependencies to be imported in CDN form, thereby reducing the size of build products and controlling loading timing.
Lazy Loading - LCP/INP#
Load components or third-party dependencies on demand to speed up page loading.
How to use#
next/dynamic
// app/page.ts
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
// Client Components:
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
// Actively disable SSR
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
// Custom loading component
const WithCustomLoading = dynamic(
() => import('../components/WithCustomLoading'),
{
loading: () => <p>Loading...</p>,
}
)
export default function ClientComponentExample() {
const [showMore, setShowMore] = useState(false)
return (
<div>
{/* Load immediately, but in a separate client bundle */}
<ComponentA />
{/* Load on demand, only when/if the condition is met */}
{showMore && <ComponentB />}
<button onClick={() => setShowMore(!showMore)}>Toggle</button>
{/* Load only on the client side */}
<ComponentC />
{/* Custom loading */}
<WithCustomLoading />
</div>
)
}
React.lazy
&Suspense
- Dynamic import using
import()
'use client'
import { useState } from 'react'
const names = ['Tim', 'Joe', 'Bel', 'Lee']
export default function Page() {
const [results, setResults] = useState()
return (
<div>
<input
type="text"
placeholder="Search"
onChange={async (e) => {
const { value } = e.currentTarget
// Dynamically load fuse.js
const Fuse = (await import('fuse.js')).default
const fuse = new Fuse(names)
setResults(fuse.search(value))
}}
/>
<pre>Results: {JSON.stringify(results, null, 2)}</pre>
</div>
)
}
- Import named exports
// components/hello.js
'use client'
export function Hello() {
return <p>Hello!</p>
}
// app/page.ts
import dynamic from 'next/dynamic'
const HelloComponent = dynamic(() =>
import('../components/hello').then((mod) => mod.Hello)
)
Analytics#
Built-in support for measuring and reporting performance metrics, it's quite detailed, my friend!!
// app/_components/web-vitals.js
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric)
})
}
// app/layout.js
import { WebVitals } from './_components/web-vitals'
export default function Layout({ children }) {
return (
<html>
<body>
<WebVitals />
{children}
</body>
</html>
)
}
Web Vitals need no further explanation.
Memory Optimization#
As applications iterate and become richer in functionality, the project will consume more and more system resources during development and building. Next.js provides some strategies to optimize:
experimental.webpackMemoryOptimizations: true
Reduce the maximum memory usage during webpack builds, in the experimental stage.next build --experimental-debug-memory-usage
Print memory usage during the build.node --heap-prof node_modules/next/dist/bin/next build
Record stack information to facilitate memory issue troubleshooting.
Monitoring#
- Instrumentation, integrating monitoring and logging tools into the project.
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel('next-app')
}
- Third-party libraries
- Google Tag Manager
// app/layout.tsx
import { GoogleTagManager } from '@next/third-parties/google'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">Google Analytics - gtag.js
<GoogleTagManager gtmId="GTM-XYZ" />
<body>{children}</body>
</html>
)
}
- Google Analytics - gtag.js
// app/layout.tsx
import { GoogleAnalytics } from '@next/third-parties/google'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
<GoogleAnalytics gaId="G-XYZ" />
</html>
)
}
- Google Maps Embed
- Youtube Embed
Render#
Server Components#
SSR can bring many benefits:
- Closer to the data source (in the same data center), faster data retrieval, and can reduce the number of client requests;
- More secure, sensitive tokens and API keys do not need to be transmitted over the network;
- Utilize caching, directly return cached data on the next request without going through data retrieval and rendering logic again, optimizing RT;
- Improve performance experience for users with poor networks or devices, purely display-type UI is rendered on the server, effectively saving the time for the browser to download, parse, and execute JS code;
- Faster initial loading and green FCP, the browser directly downloads prepared HTML, presenting page content immediately, reducing white screen time;
- SEO;
- Further application of streaming rendering strategies;
By default, components in Next.js are server components. It is important to note that SSR cannot use the following APIs:
- Event Listener
- DOM/BOM API
- useContext, useEffect, useState, etc.
How to render#
First, on the server:
- React renders the React Component into
React Server Component Payload
(RSC Payload
) - Next.js uses the
RSC Payload
and Client Component JavaScript to render HTML on the server (similar torenderToString
?renderToPipeableStream
?)
Then on the client:
- Immediately render the page upon receiving the HTML
- Use the
RSC Payload
to reconcile the client and server component trees, updating the DOM - Hydrate client components, binding events to make them interactive
The rendering methods for Server Components are divided into Static Render, Dynamic Render, and Stream Render.
Static Render#
Rendered as static content on the server during the build, cached, and directly returned for subsequent requests. Suitable for purely static display UIs or UIs that do not change and are indifferent to users.
Dynamic Render#
In contrast to static rendering, if data has personalized characteristics and relies on obtaining data such as Cookie, searchParams, etc., on each request, it needs to be rendered in real-time for each request. The switch between dynamic rendering and static rendering is automatically completed by Next.js, which will choose the appropriate rendering strategy based on the APIs used by the developer, such as Dynamic Functions:
cookies()
headers()
unstable_noStore()
unstable_after()
searchParams prop
Routes using these dynamic rendering APIs will adopt a dynamic rendering strategy.
Stream Render - TTFB & FCP & FID#
Non-streaming rendering#
SSR must go through a serialized, sequentially blocking process of A, B, C, D. The server can only start rendering HTML after obtaining all data, and the client can only start hydration after receiving the complete JS.
Streaming rendering#
Divides different parts of the page into different chunks, and the server progressively returns them, rendering and displaying as soon as they are returned, without waiting for all data to be fully prepared. React Components are naturally independent chunks; chunks that do not depend on asynchronous data fetching can be returned directly, while the rest are returned one by one.
Client Components#
The benefits of client-side rendering:
- Provides interactivity, allowing direct use of State, Effect, EventListener
- Can directly call BOM, DOM APIs
Declaring a client component is very simple; just write the'use client'
directive at the beginning of the file to inform Next.js that this component is a client component:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
If a client component is not declared, it defaults to a server component, and using client APIs will result in an error:
How to render#
For the initial page load: Consistent with Server Components.
For subsequent navigation: Similar to client-side SPA navigation, without making new requests to the server to generate HTML, but using the RSC Payload to complete the navigation.
When to use SC and when to use CC#
PPR#
Partial Prerender, Next.js will try to pre-render as many components as possible during the build. When encountering asynchronous components wrapped in Suspense, the UI in the fallback will also be pre-rendered first. The benefit of this is to merge multiple requests, reducing the browser's network request waterfall.
Loading#
loading.tsx
is a special file in Next.js, implemented based on Suspense
, for route and component-level loading states. You can implement your own SkeletonComponent
or SpinnerComponent
as fallback UI during asynchronous component loading, effectively improving UX.
Finally#
The Next.js official website has many design philosophies and concepts worth pondering and reflecting on repeatedly. Recently, I have seen many independent developers quickly develop products based on Next.js, bringing their ideas to life. It is a Next Gen Web Framework that provides a full range of services from development, deployment, performance to backend, giving it a bit of a full-stack flavor.
The end.