alifeiliu

alifeiliu

哪有什么无限进步,能进一步是一步
github
x
discord server
email
strava
bilibili

Thanks for opening my eyes, Next.js !

Recently completed 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 implementing and deploying a dashboard project on Vercel. Check out my Demo 👉 Entrance

image

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 focus on translation but rather summarizes features related to performance/experience optimization. Now, let's get into the main content.

What is Next.js#

image

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 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

Coinciding with the Next.js 2024 Developer Summit, I got a virtual entry pass 😬:

image

The "built-in performance optimization 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 with 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 adaptability: 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 backup content when the video tag is not supported
  • Provide subtitles and captions: Accessibility support
  • Compatibility video controls: Support keyboard operations for 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:

  • Supports 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#

  1. 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>
    </>
  )
}
  1. 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);
}
  1. 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 functionalities:

  • 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, follow, cache
  • Supports asynchronous generation of MetaData

How to use#

  1. Static configuration
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: '...',
  description: '...',
}
 
export default function Page() {}
  1. 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, and Next.js will wait for the asynchronous execution to complete before returning the UI stream, ensuring that the first segment of the stream response contains the correct <head> tag.

Scripts - LCP & INP#

The optimization capabilities provided by the component:

  • Third-party dependencies can be shared across 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#

  1. Layout/page scripts, load third-party dependencies only under specific Layout/Page, which can 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" />
    </>
  )
}
  1. Adjust loading strategies
  • beforeInteractive: Before page hydration
  • afterInteractive: After page hydration
  • lazyLoad: Load during browser idle time
  • worker (experimental): Enable Web worker loading; non-CRP JS should be executed in the Worker, yielding to the main thread:

image

  1. Supports inline scripts
    id prop required, allowing 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')`,
  }}
/>
  1. Supports three callbacks
  • onLoad: Script loading completed
  • onReady: After script loading is completed, each time the component is mounted
  • onError: 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#

Next.js optimized third-party dependency list, imported on demand, not in full.

External#

Next.js externalized third-party dependency list, similar to webpack externals, specifies dependencies to be imported in CDN form, thereby reducing the size of the build product and controlling loading timing, etc.

Lazy Loading - LCP/INP#

Load components or third-party dependencies on demand to speed up page loading.

How to use#

  1. 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>
  )
}
  1. React.lazy & Suspense
  2. 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>
  )
}
  1. 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 features, the project will consume more and more system resources during development and building. Next.js provides some strategies to optimize:

  1. experimental.webpackMemoryOptimizations: true
    Reduces the maximum memory usage during webpack builds, in the experimental phase.
  2. next build --experimental-debug-memory-usage
    Prints memory usage during the build.
  3. node --heap-prof node_modules/next/dist/bin/next build
    Records stack information for troubleshooting memory issues.

Monitoring#

  1. Instrumentation, integrating monitoring and logging tools into the project.
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
 
export function register() {
  registerOTel('next-app')
}
  1. 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, returning cached data directly 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 UIs are 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:

  1. React renders the React Component into React Server Component Payload (RSC Payload)
  2. Next.js uses the RSC Payload and Client Component JavaScript to render HTML on the server (similar to renderToString? renderToPipeableStream?)

Then on the client:

  1. Immediately render the page upon receiving the HTML
  2. Use the RSC Payload to reconcile the client and server component trees, updating the DOM
  3. 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. It is 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 the data has personalized characteristics, relying on each request to obtain data such as Cookie, searchParams, etc., it needs to be rendered in real-time for each request. The switch between dynamic rendering and static rendering is automatically handled 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 dynamic rendering strategies.

Stream Render - TTFB & FCP & FID#

Non-streaming rendering#

image

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#

image

Divides different parts of the page into different chunks, with the server progressively returning them, rendering and displaying as they are returned, without waiting for all data to be prepared. React Components are naturally independent chunks, and chunks that do not rely on asynchronous data fetching can be returned directly, with the rest returned sequentially.

image

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:

image

How to render#

For the initial full-page load: the same as Server Component.
For subsequent navigation: similar to client-side SPA navigation, without making a new request to the server for generating HTML, but using the RSC Payload to complete navigation.

When to use SC and when to use CC#

image

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. The benefit of this is to merge multiple requests, reducing the waterfall of browser network requests.

image

Loading#

image

loading.tsx is a special file in Next.js, implemented based on Suspense, providing loading states at the route and component levels. You can implement your own SkeletonComponent or SpinnerComponent as fallback UI during asynchronous component loading, effectively improving UX.

Conclusion#

The Next.js official website has many design philosophies and concepts worth pondering and reflecting on. Recently, I have seen many independent developers rapidly develop products based on Next.js, bringing their ideas to fruition. It is a Next Gen Web Framework that provides a one-stop service from development, deployment, performance to backend, with a bit of a full-stack flavor.

The end.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.