Next.js - latest(v14 & V15) 
RSC与SSR、SSG, ISR, Partial Prerendering 
SSG是后端编译时方案。使用SSG的业务,后端代码在编译时会生成HTML(通常会被上传CDN)。当前端发起请求后,后端(或CDN)始终会返回编译生成的HTML。
RSC与SSR则都是后端运行时方案。也就是说,他们都是前端发起请求后,后端对请求的实时响应。根据请求参数不同,可以作出不同响应。
同为后端运行时方案,RSC与SSR的区别主要体现在输出产物:
- 类似于SSG,SSR的输出产物是HTML,浏览器可以直接解析
- RSC会流式输出一种类JSON的数据结构,由前端的React相关插件解析
SSG 
Static Site Generation, SSG 会在构建阶段,就将页面编译为静态的 HTML 文件。
SSR 
在app路由下,只要我们的组件是使用 async 进行了修饰的,都会默认开启SSR.
ISR 
SSG 的优点就是快,部署不需要服务器,任何静态服务空间都可以部署,而缺点也是因为静态,不能动态渲染,每添加一篇博客,就需要重新构建。所以有了ISR,增量静态生成,可以在一定时间后重新生成静态页面,不需要手动处理。
app路由实现ISR,需要利用到fetch的缓存策略,在请求接口的时候,添加参数revalidate,来指定接口的缓存时间,让它在一定时间过后重新发起请求。
export default async function PokemonName({
  params
}: {
  params: { name: string }
}) {
  const { name } = params
  // revalidate表示在指定的秒数内缓存请求,和pages目录中revalidate配置相同
  const res = await fetch('http://localhost:3000/api/pokemon?name=' + name, {
    next: { revalidate: 60, tags: ['collection'] },
    headers: { 'Content-Type': 'application/json' }
  })
  return <p>...</p>
}但是在通常情况下,静态页面更新实际上没有那么频繁,但是有些情况有需要连续更新(发布博客有错别字),这个时候其实需要一种能手动更新的策略,来发布指定的静态页面。
On-demand Revalidation(按需增量生成) 
NextJS提供了更新静态页面的方法,可以在 app 目录下新建一个 app/api/revalidate/route.ts接口,用于实现触发增量更新的接口。
为了区分需要更新的页面,可以在调接口的时候传入更新的页面路径,也可以传入在fetch请求中指定的collection变量。
import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath, revalidateTag } from 'next/cache'
// 手动更新页面
export async function GET(request: NextRequest) {
  // 保险起见,这里可以设置一个安全校验,防止接口被非法调用 simple way, 不能设置为NEXT_PUBLIC_xx,会被打包到浏览器可访问
  if (request.query.secret !== process.env.UPDATE_SSG_SECRET) {
    return NextResponse.json(
      { data: error, message: 'Invalid token' },
      {
        status: 401
      }
    )
  }
  const path = request.nextUrl.searchParams.get('path') || '/pokemon/[name]'
  // 这里可以匹配fetch请求中指定的collection变量
  const collection =
    request.nextUrl.searchParams.get('collection') || 'collection'
  // 触发更新
  revalidatePath(path)
  revalidateTag(collection)
  return NextResponse.json({
    revalidated: true,
    now: Date.now(),
    cache: 'no-store'
  })
}如果数据库中的内容有修改,访问http://localhost:3000/api/revalidate?path=/pokemon/Charmander, 就可以实现/pokemon/Charmander这个路由的手动更新。
兜底策略 
静态页面在生成期间,如果用户访问对应路由会报错,这时需要有一个兜底策略来防止这种情况发生。
Next.js在组件中指定了dynamicParams的值(true默认),当dynamicParams设置为true时,当请求尚未生成的路由段时,页面将通过SSR这种方式来进行渲染。
export const dynamicParams = truePartial Prerendering 
Combine static and dynamic content in the same route. This improves the initial page performance while still supporting personalized, dynamic data.
ENV 
- 默认情况下,环境变量只能在server端获取
- 以NEXT_PUBLIC_开始的环境变量,会在打包的时候替换成固定的值。一定要按这种格式获取process.env.[variable],variable不能是dynamic的
- 非NEXT_PUBLIC_会保留原始的代码,比如打包后的代码也是process.env.DB_PASSWORD
load Order
- process.env
- .env.$(NODE_ENV).local
- .env.local (Not checked when NODE_ENV is test.)
- .env.$(NODE_ENV)
- .env
.env 文件会如果有会被打包进去, local文件不会
比如DB_PASSWORD="123" NEXT_PUBLIC_API_URL="1231" node dist/standalone/server.js启动服务,DB_PASSWORD="123"优先级最高,但NEXT_PUBLIC_API_URL不会变,还是打包时的替换值
Data fetch 
(RSC) 数据获取 
- 特点:在服务器端执行,直接返回HTML给客户端,并且是 Streaming UI。
- 适用场景:使用支持RSC的框架(如Next.js)。
- 优势:避免客户端-服务器通信往返,直接访问服务器端数据源。
import { getPosts } from '@/features/post/queries/get-posts'
const PostsPage = async () => {
  const posts = await getPosts()
  return (
    <div>
      <h1>React Server Component</h1>
      <ul>{posts?.map((post) => <li key={post.id}>{post.title}</li>)}</ul>
    </div>
  )
}
export default PostsPageReact Query 
在client, 即RCC组件中使用
- 特点:客户端数据获取,提供hooks用于数据获取、缓存和更新。
- 适用场景:客户端渲染的React应用(SPA)。
- 优势:处理缓存、竞态条件和陈旧数据
- 不能实现stream ui(也不用提,本身就是client发起请求的)
这里的getPosts和服务端的有所区别:要使用a remote API over HTTP / endpoint
export const getPosts = async () => {
  const response = await fetch('/api/posts')
  return response.json()
}而RSC就可以直接访问数据库:
export const getPosts = async () => {
  return await db.query('SELECT * FROM posts')
}'use client'
import { getPosts } from '@/features/post/queries/get-posts'
import { useQuery } from '@tanstack/react-query'
const PostsPage = () => {
  const { data: posts } = useQuery({
    queryKey: ['posts'],
    queryFn: getPosts
  })
  return (
    <div>
      <h1>React Query</h1>
      <ul>{posts?.map((post) => <li key={post.id}>{post.title}</li>)}</ul>
    </div>
  )
}
export default PostsPageRSC + RCC 
- 特点:服务器端获取初始数据,客户端继续使用React Query获取数据。初始化时是 streaming ui
- 适用场景:需要初始数据快速加载和客户端无限滚动等高级数据获取模式。
- 优势:结合服务器端和客户端数据获取的优势。
import { getPosts } from '@/features/post/queries/get-posts'
import { PostList } from './_components/post-list'
const PostsPage = async () => {
  const posts = await getPosts()
  return (
    <div>
      <h1>React Server Component + React Query</h1>
      <PostList initialPosts={posts} />
    </div>
  )
}
export default PostsPage'use client'
import { getPosts } from '@/features/post/queries/get-posts'
import { Post } from '@/features/post/types'
import { useQuery } from '@tanstack/react-query'
type PostListProps = {
  initialPosts: Post[]
}
const PostList = ({ initialPosts }: PostListProps) => {
  const { data: posts } = useQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
    initialData: initialPosts
  })
  return <ul>{posts?.map((post) => <li key={post.id}>{post.title}</li>)}</ul>
}
export { PostList }可以用Server Actions, 在server和client重复使用,这样就不用反复声明了,但要注意鉴权如果接口需要的话
也可以从RSC中传入一个promise到RCC,当作init promise, 并用Suspense wrap RCC, 也能实现streaming UI,并且client也能update data
但 client update promise时,组件也会fallback 到最近的
Suspense,组件就会消失而显示Suspense 的fallback,可以通过 useTransition, refer: Preventing unwanted loading indicators
// in RCC
const [promise, setPromise] = useState(initPromise)
const data = use(promise)
// update promise to get new data due to some user interaction
const onClick = () => {
  setPromise()
}use Api 
use(Promise)
- 允许将Promise从服务器组件传递到客户端组件。
- 适用场景:需要在客户端组件中解析服务器组件的异步操作。
- 优势:避免阻塞服务器组件的渲染,也能实现streaming UI。
- 也可以将Promise管理为state,更新promise 重新获取数据
- use会找最近的Suspense组件显示fallback
import { Suspense } from 'react'
import { getPosts } from '@/features/post/queries/get-posts'
import { PostList } from './_components/post-list'
const PostsPage = () => {
  const postsPromise = getPosts()
  return (
    <div>
      <h1>use(Promise) RSC</h1>
      <Suspense>
        <PostList promisedPosts={postsPromise} />
      </Suspense>
    </div>
  )
}
export default PostsPage'use client'
import { use } from 'react'
import { Post } from '@/features/post/types'
type PostListProps = {
  promisedPosts: Promise<Post[]>
}
const PostList = ({ promisedPosts }: PostListProps) => {
  const posts = use(promisedPosts)
  return <ul>{posts?.map((post) => <li key={post.id}>{post.title}</li>)}</ul>
}
export { PostList }Another example:
const UseHookExample = () => {
  const [findPetsByStatusPromise, setFindPetsByStatusPromise] = useState(() =>
    findPetsByStatus({ status: undefined })
  )
  return (
    <div>
      <h3 className="my-2">Find Pets By Status</h3>
      <Select
        onValueChange={(value: FindPetsByStatusStatus) => {
          setFindPetsByStatusPromise(findPetsByStatus({ status: value }))
        }}
      >
        <SelectTrigger className="w-[230px]">
          <SelectValue placeholder="Select Status" />
        </SelectTrigger>
        <SelectContent>
          <SelectGroup>
            <SelectLabel>Status</SelectLabel>
            {Object.entries(FindPetsByStatusStatus).map(([key, text]) => (
              <SelectItem value={key} key={key}>
                {text}
              </SelectItem>
            ))}
          </SelectGroup>
        </SelectContent>
      </Select>
      <Separator className="mt-4" />
      <Suspense
        fallback={
          <p className="my-4 text-sm text-muted-foreground">loading...</p>
        }
      >
        <PetsStatusList findPetsByStatusPromise={findPetsByStatusPromise} />
      </Suspense>
    </div>
  )
}
const PetsStatusList = ({
  findPetsByStatusPromise: initP
}: {
  findPetsByStatusPromise: ReturnType<typeof findPetsByStatus>
}) => {
  const [counter, setCounter] = useState(0)
  // 也可以内部自己管理promise
  // const [findPetsByStatusPromise, setFindPetsByStatusPromise] = useState(initP)
  const resp = use(findPetsByStatusPromise)
  return (
    <div>
      <Button
        onClick={() =>
          // 自己更新
          setFindPetsByStatusPromise(
            findPetsByStatusPromise({ status: FindPetsByStatusStatus.sold })
          )
        }
      >
        update
      </Button>
      {resp.map(({ name, status, photoUrls }) => (
        <ul className="my-2 gap-1 py-2" key={name}>
          <li>name: {name}</li>
          <li>status: {status}</li>
          <li className="text-sm text-muted-foreground">
            photoUrls: {photoUrls.join(',')}
          </li>
        </ul>
      ))}
      <p>counter: {counter}</p>
      <Button onClick={() => setCounter(counter + 1)}>add</Button>
    </div>
  )
}tRPC 
tRPC 类型安全数据获取
- 特点:提供类型安全的API层。
- 适用场景:需要类型安全的全栈解决方案。
- 优势:避免运行时错误,提升开发体验。
Chore 
Streaming Server Rendering with Suspense 
想要streaming一定要加Suspense,如果不在对应的async 组件套suspense,会一直冒泡到上层去找Suspense,可能就没有streaming的效果
react use和 Suspense
NextJS 代理服务器阻塞了SSE的流式数据传输 
SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。
解决办法:服务端接口的 Response Header 内通过设置Cache-Control 为 no-cache, no-transform
revalidatePath是在server action使用
createPortal not working 
使用createPortal api, 第二个参数是document.body时可用,但当是document.getElementById('customer_id'),customer_id的dom是一个组件,添加在layout时,就添加不上了, 即使组件是use client也不行,并且还判断了也不work
function AnchorIndicator() {
  const portalNode =
    typeof window !== 'undefined'
      ? document.getElementById(SECTION_ANCHOR_DOM_ID)
      : null
  return (
    <div>
      something
      {
        portalNode
          ? createPortal(<div>xx</div>, portalNode)
          : null
      }
      </div>
  )
}报错: Hydration failed because the server rendered HTML didn't match the client...., 组件加了use client,也会ssr。
点击查看代码
"use client"
import { useRef, useEffect, useState } from "react";
import { createPortal } from "react-dom";
export default function ClientOnlyPortal({ children, selector }) {
  const ref = useRef();
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    ref.current = document.querySelector(selector);
    setMounted(true);
  }, [selector]);
  return mounted ? createPortal(children, ref.current) : null;
}'use client'
import { useEffect, useId, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { cn } from '@/lib/utils'
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '../ui/tooltip'
import { SECTION_ANCHOR_DOM_ID } from './constant'
type SectionTitleProps = {
  title: string
  className?: string
}
export function SectionTitle({ title, className }: SectionTitleProps) {
  const id = useId()
  const [mounted, setMounted] = useState(false)
  const portalNode =
    typeof window !== 'undefined'
      ? document.getElementById(SECTION_ANCHOR_DOM_ID)
      : null
  console.log('portalNode', portalNode)
  useEffect(() => setMounted(true), [])
  const handleClick = () => {
    const element = document.getElementById(id)
    if (!element) return
    // const top = element.getBoundingClientRect().top + window.scrollY
    element.scrollIntoView({
      behavior: 'smooth',
      block: 'start',
    })
  }
  return (
    <h2
      id={id}
      className={cn(
        'scroll-m-16 text-xl font-semibold tracking-tight text-gray-800',
        className,
      )}
      data-section-title={title}
    >
      {title}
      {portalNode
        ? createPortal(
            <div
              className="flex flex-col items-center"
              data-section-id={id}
            >
              <TooltipProvider>
                <Tooltip>
                  <TooltipTrigger asChild>
                    <button
                      type="button"
                      className="-m-2 cursor-pointer p-2"
                      onClick={handleClick}
                    >
                      <span
                        className={cn(
                          'inline-block size-3 rounded-full border-2 transition-colors duration-200',
                          'border-gray-300 bg-white data-[active=true]:border-primary data-[active=true]:bg-primary',
                        )}
                        data-active="false"
                      />
                    </button>
                  </TooltipTrigger>
                  <TooltipContent side="left">
                    <p>{title}</p>
                  </TooltipContent>
                </Tooltip>
              </TooltipProvider>
            </div>,
            portalNode,
          )
        : null}
    </h2>
  )
}原理 
Libraries 
- nuqs Type-safe search params state manager for Next.js - Like React.useState, but stored in the URL query string.
- next-safe-action Type safe and validated Server Actions in your Next.js project.
Hydration Failed 
水合错误,“水合(Hydration)指 React 在客户端把预渲染的 HTML 与组件树进行匹配,重建内部状态并绑定事件处理器,从而将其激活为完全可交互应用的过程。”
常见原因
- HTML 元素错误嵌套 - <p>嵌套在另一个- <p>元素中
- <div>嵌套在- <p>元素中
- <ul>或- <ol>嵌套在- <p>元素中
- 交互式内容(interactive-content)不能嵌套,比如 <a>不能嵌套在<a>标签中,<button>不能嵌套在<button>标签中等等。
 
- 渲染时使用 - typeof window !== 'undefined'等判断。 eg:
'use client'
export default function App() {
  const isClient = typeof window !== 'undefined';
  return <h1>{isClient ? 'Client' : 'Server'}</h1>
}这个错误只会出现在客户端组件中,服务端渲染的时候,因为在 Node 环境,isClient 为 false,返回 Server,而在客户端的时候,会渲染成 Client,渲染内容不一致导致出现水合错误。
- 渲染时使用客户端 API 如 window、localStorage 等
'use client'
export default function App() {
  return <h1>{typeof localStorage !== 'undefined' ? localStorage.getItem("name") : ''}</h1>
}- 使用时间相关的 API,如 Date
'use client'
export default function App() {
  return <h1>{+new Date()}</h1>
}原因在于服务端渲染和客户端渲染的时间不一致。客户端组件它会先在服务端进行一次预渲染,传给客户端后还要进行一次水合,添加事件处理程序,最后根据客户端事件进行更新。
- 浏览器插件导致
- 比如 IOS 的网页会尝试检测文本内容中的电话号码、邮箱等数据,将它们转为链接,方便用户交互,这也会导致水合错误。
如果遇到这个问题,可以使用 meta 标签禁用:
<meta
  name="format-detection"
  content="telephone=no, date=no, email=no, address=no"
/>解决办法 
使用 useEffect 
'use client'
import { useState, useEffect } from 'react'
export default function App() {
  const [isClient, setIsClient] = useState(false)
 
  useEffect(() => {
    setIsClient(true)
  }, [])
  
  return <h1>{isClient ? 'Client' : 'Server'}</h1>
}禁用特定组件的 SSR 渲染 
需要借助 Next.js 提供的 dynamic 函数。
import dynamic from 'next/dynamic'
 
const NoSSR = dynamic(() => import('./no-ssr'), { ssr: false })
 
export default function Page() {
  return (
    <div>
      <NoSSR />
    </div>
  )
}使用 suppressHydrationWarning 取消错误提示 
自定义hooks 
其实就是使用useEffect,对客户端的api做封装,比如useWindowSize, useLocalStorage等。
useMounted,当挂载的时候再渲染内容:
'use client'
import { useState, useEffect } from 'react'
export function useMounted() {
  const [mounted, setMounted] = useState(false)
  useEffect(() => {
    setMounted(true)
  }, [])
  return mounted
}
export default function Page() {
  const mounted = useMounted()
  if (!mounted) return null
  return (
    <div>
      <h1>{+new Date()}</h1>
      <h1>{localStorage.getItem("name")}</h1>
    </div>
  )
}Error: document is not defined 
或者window is not defined
本质原因是在服务端调用了客户端 API 导致报错。但使用 'use client' 指令并不一定能够解决问题,'use client'指令只能说明组件可以运行在客户端,但并不说明组件只运行在客户端。客户端组件会在服务端进行预渲染,如果要取消掉这个预渲染,可以使用 dynamic 这个函数动态加载客户端组件。 同时,在自己的项目中使用客户端 API 如 window、document 的时候,也要注意避免出现这类错误。可以使用 useEffect、typeof window、dynamic、useMounted 等方式进行妥善处理。
- 使用客户端组件,即添加'use client',但要注意:Moving Client Components Down the Tree。最佳实践的角度来看,我们应该尽可能减少客户端组件的范围(对应组件树中的位置下移)
- 动态导入
import dynamic from 'next/dynamic'
 
const WithCustomLoading = dynamic(
  () => import('../components/WithCustomLoading'),
  {
    loading: () => <p>Loading...</p>,
  }
)
 
export default function Page() {
  return (
    <div>
      <WithCustomLoading />
    </div>
  )
}动态加载本质上是 Suspense 和 React.lazy 的复合实现。
性能优化 
- Moving Client Components Down the Tree
先分析
使用工具 
- google控制台的lighthouse,类似体检报告,可以看到各项的打分,包括performance,Accessibility,Best Practices,SEO 
 
- google控制台的performance,可以Record,DevTools 会生成一张瀑布图 + 主线程火焰图。 - Network/Waterfall 看每个资源的加载时机、阻塞时间
- Timings 轨道:FP、FCP、LCP、TTI 自动打点
- Main 火焰图:找右上角带 红色小三角 的长任务(>50 ms)
- Frames 轨道:若出现红色长条 → 丢帧,需优化脚本执行
 
- google控制台的网络network,check资源加载情况 
- 选择专业性能指标检测工具,比如 pagespeed.web.dev/ 
- @next/bundle-analyzer可视化的检查页面和打包的资源大小
- react scan Scan for React performance issues and eliminate slow renders in your app 
方法 
- 合理的拆分组件,搭配React server component + Suspense,实现streaming ui,是用户更快的看到页面。
- 要精细话区分critical data还是non critical data,避免server block http response
缓存 
- React.cache,Server Component 中使用,把昂贵的数据获取或计算函数包一层 cache,同一 HTTP 请求里无论被多少个 Server Component 调用,都只执行一次,其余直接读缓存。
- Next.js的缓存策略。fetch等Caching in Next.js,“Next.js 默认帮你缓存到极致,你需要做的只是告诉它什么时候不要缓存。”
- 静态资源缓存,CDN缓存,http缓存
图片,css进行优化 
- 提供大小合适的图片可节省移动数据网络流量并缩短加载用时,图片压缩,转化为webp格式减小体积
- 懒加载 Lazy load
- 小图片用base64代替
- 多使用nextjs的Image组件,提供了多个优化配置,比如priority,decoding
- 减少重排和重绘,避免布局偏移例如:动态设置css导致的布局偏移,可以使用占位符来解决或者固定
组件进行懒加载 
在client组件中使用,按需加载nextjs提供了dynamic,它包含了React.lazy和Suspense,是需要时再去加载,而不是可视区内在加载,适合对子组件进行使用。
import dynamic from "next/dynamic";
const FedeInSection = dynamic(() => import('@/components/FedeInSection'));比如,可能通过@next/bundle-analyzer首屏的资源打包了,用户登录formModal,改为用户点击Log In时再去加载对应展示的组件 。
'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'))
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
 
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 />
    </div>
  )
}第三方库包大小的优化 
- 比如Moment替换为dayjs
- 确保tree shaking 是work的,使用lodash/es,nextjs也提供optimizePackageImports配置。
组件拆分精量化 
将首屏的组件模块进行拆分复用在通过dynamic函数加载
第三方脚本工具script优化 
- defer延迟加载
preload 预加载 
Partial Prerendering 但15还是实验阶段 
Enable http2 
HTTP/2 最大的好处是 显著提升了页面加载性能
- 多路复用 (Multiplexing),这是 HTTP/2 最核心的改进。 - HTTP/1.1: 存在“队头阻塞”问题。在单个 TCP 连接上,浏览器一次只能处理一个请求,必须等上一个请求响应后才能发送下一个。尽管浏览器会开启多个连接(通常是6个)来缓解,但依然有上限。
- HTTP/2: 允许在同一个 TCP 连接上同时发送和接收多个请求和响应,它们可以交错进行而不会相互阻塞。
 
对 Next.js 的好处:
Next.js 应用通过代码分割(Code Splitting)会产生许多小的 JavaScript 和 CSS 文件块。使用 HTTP/1.1 时,加载这些大量的小文件效率很低。而 HTTP/2 的多路复用机制可以一次性、高效率地并行加载所有这些资源,极大地缩短了页面渲染完成的时间。
- 头部压缩 (Header Compression) - HTTP/1.1: 每次请求都会发送大量重复的、未经压缩的纯文本头部信息,造成了不必要的网络开销。
- HTTP/2: 使用 HPACK 算法对请求头进行压缩,大大减少了数据传输量,尤其是在一个页面有数十个资源请求时,效果非常显著。
 
对 Next.js 的好处:
Next.js 应用不仅请求 JS/CSS,还可能包含大量的 API 请求、图片等资源。头部压缩降低了每个请求的开销,累积起来可以节省可观的带宽和时间。
- 二进制协议 (Binary Protocol) - HTTP/1.1: 是一个纯文本协议,可读性好但处理起来效率较低,且容易出错。
- HTTP/2: 采用二进制格式传输数据,计算机解析起来更快、更高效、更不容易出错。
 
对 Next.js 的好处:
这是底层协议的优化,直接提升了数据传输和处理的效率,为整体性能带来了增益。
实现方式 
- 部署到vercel的话:deployment is automatically served over HTTP/2, compressed with Brotli/Gzip, and downloaded from a CDN edge nearest to your end-user.
- self-hosting - 通过Nginx暴露和HTTP/2服务,然后nginx和node之间还是HTTP/1.1, 这样next.js就不需要修改:Configure your application behind Nginx (or another reverse proxy) for optimum performance. Nginx should handle HTTP/2, the HTTPS termination, Gzip compression, etc. Only the localhost traffic (between Nginx and Node) would be over HTTP/1.1, where the latency is effectively zero.
- 使用自定义node serve with http2: Serve your application in production directly from a Node.js server, example: with-http2
 
- 通过Nginx暴露和
http2 server.js
/**
 * package.json script:
   "dev": "node server.js",
   "build": "next build",
   "start": "cross-env NODE_ENV=production node server.js"
 */
const next = require("next");
const http2 = require("node:http2");
const { parse } = require("node:url");
const fs = require("node:fs");
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
// Init the Next app:
const app = next({ dev });
// Create the secure HTTPS server:
// Don't forget to create the keys for your development
const server = http2.createSecureServer({
  key: fs.readFileSync("localhost-privkey.pem"),
  cert: fs.readFileSync("localhost-cert.pem"),
});
const handler = app.getRequestHandler();
app.prepare().then(() => {
  server.on("error", (err) => console.error(err));
  server.on("request", (req, res) => {
    const parsedUrl = parse(req.url, true);
    handler(req, res, parsedUrl);
  });
  server.listen(port);
  console.log(`Listening on HTTPS port ${port}`);
});国际化i18n 
- lingui 核心就是写一个语言,然后它自动生成其他多语言适配。