🍂落页
登 录

Next.js 笔记

  • 安装及项目结构
  • 路由
  • 获取数据
  • 渲染
  • 缓存
  • 优化
🍂落页
TALAXY
渲染
🍂落页
TALAXY

渲染

渲染
  • 服务端组件
  • 客户端组件
  • 两端搭配
  • Edge 和 Node.js 运行时
  • 参考

应用的渲染可以有两种环境:客户端和服务端。两者都有自己的特性和限制,不能说孰优孰劣。

在 Web 开发中,可以用 网络边界 的概念来划分这两种环境。可以用 React 的 "use client" 来定义网络边界。或者用 "use server" 来告诉 React 在服务器上做一些计算性的工作。

服务端组件

React 服务端组件可以让你在服务器上进行 UI 渲染及缓存。在 Next.js 上,渲染工作则会根据路由片段的划分来启用流式渲染或部分渲染。

有三种服务端渲染的策略:静态渲染、动态渲染、流式渲染。

Next.js 默认使用服务端组件,无需额外配置。

服务端渲染的优势

  • 数据获取 - 服务器通常离数据资源更近,缩短了请求时间。也可以减少客户端请求的数量。
  • 安全 - 服务端组件会将敏感数据和逻辑留存在服务端上,比如 token 或 API Key ,避免暴露给客户端。
  • 缓存 - 服务端会对渲染结果进行缓存,一定程度上减少了渲染的工作量和请求次数。
  • 打包体积 - 一些大的依赖可以留存在服务端,来减少客户端包体积。
  • 首屏加载时间 - 服务端可以生成 HTML ,让用户立即看到页面。
  • SEO - 渲染的 HTML 可以让搜索引擎或社交网站的爬虫索引你的页面。
  • 流式渲染 - 服务端组件允许你将渲染工作进行拆分,并逐步渲染传输给客户端,确保用户能立即看到页面的一部分。

渲染步骤

Next.js 使用 React 的 API 进行服务端渲染。渲染工作会根据路由片段和 Suspense 进行拆分。

每一拆分的块的渲染步骤如下:

  1. 在服务端上:
    1. React 将服务端组件渲染为一种特殊的数据格式,称为 RSC Payload(React Server Component Payload)。
    2. Next.js 拿着 RSC Payload 和客户端组件的 JS 代码来渲染 HTML 。
  2. 接着,在客户端上:
    1. 会立即加载一个无法交互的 HTML ,即首屏页面。
    2. RSC Payload 会比对客户端和服务端的组件树,来更新 DOM 。
    3. 根据 JS 代码来 hydrate 客户端组件,使应用可交互。

RSC Payload 通常会包含这些信息:

  • 服务端组件的渲染结果;
  • 客户端组件的占位符及其 JS 文件的引用地址;
  • 所有服务端组件传给客户端组件的参数。

服务端渲染策略

静态渲染

Next.js 默认采取静态渲染。路由会在构建时(或当数据重新校验后)被渲染好。渲染结果会被缓存并且可以推送到 CDN 上。

静态渲染通常用于与用户数据不相关的页面,比如静态博客页面。

动态渲染

当页面与用户数据相关,或当请求中带有信息时(比如 URL 参数、cookie 等),通常使用动态渲染。在动态渲染中,路由会对每次请求都进行渲染。

路由并非只能是纯静态或纯动态。在 Next.js 中,RSC Payload 和请求数据是分别缓存的。

当面对一个动态函数或者未缓存数据请求时,Next.js 会对整个路由进行动态渲染。这些都是自动的,开发者不需要手动设置静态或动态渲染。

动态函数 会依赖一些在请求时才可获知的信息,比如用户 cookie、请求头、查询参数。在 Next.js 中具体有:

  • cookies() 和 headers() - 会直接将路由切为动态渲染;
  • useSearchParams() - 若在客户端组件中使用了这一函数,会跳过静态渲染,并回退到最近的 Suspense 上。因此建议将这类组件包裹在 Suspense 中,来保证上层组件可以正常静态渲染;
  • searchParams - 使用该页面属性会将路由切为动态渲染。

流式渲染

Next.js 默认开启了流式渲染,可以用 loading.js 及 Suspense 启用这一特性。

客户端组件

在 Next.js 中需在文件最顶部加上 "use client" 来将组件声明为客户端组件。如果在服务端组件中使用了客户端才可用的 API(比如 useState、onClick 等),会报错。

使用客户端组件的好处有:

  • 交互 - 可以使用 state、effect、事件监听等提供用户反馈的 API ;
  • 浏览器 API - 比如 localStorage 等。

渲染方式

不同的页面请求方式,客户端组件的渲染方式不同。请求方式有 整页加载 和 导航切换 。

整页加载

整页加载,即首次打开页面或者采用了浏览器刷新。为了优化首屏加载,Next.js(借助 React)均会将客户端和服务端组件渲染为 HTML ,无需等待客户端组件的 JS 包。

导航切换

通过导航切换打开的页面,客户端组件会完全由客户端渲染。客户端拿到客户端组件的 JS 包后,React 会根据 RSC Payload 来比对客户端和服务端组件树,然后更新 DOM 。

两端搭配

服务端组件注意点

  • 传递数据 - 在服务端不可以利用 React Context 来传递参数,但可以利用 fetch 或 React 中的缓存机制来获取数据。更多见 React 中的 记忆化 。

  • 防止服务端代码进入客户端 - 为了防止服务端代码(比如接口请求)意外地在客户端代码中使用,可以引入 server-only 库(需要单独安装依赖):

    import 'server-only'
    
    export async function getData() {
        const res = await fetch('https://external-service.com/data', {
            headers: {
                authorization: process.env.API_KEY,
            },
        })
        
        return res.json()
    }
    
  • 第三方库引入 - 许多第三方库可能没有在组件上声明 "use client" ,需要我们自行封装一下:

    'use client'
    
    import { Carousel } from 'acme-carousel'
    
    export default Carousel
    
  • Context - 在服务端代码中使用 Context 时,该 Context 应当在一个单独的文件中声明,并声明为客户端代码:

    'use client'
    
    import { createContext } from 'react'
    
    export const ThemeContext = createContext({})
    
    export default function ThemeProvider({ children }) {
        return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
    }
    

客户端组件注意点

  • 下沉客户端组件代码 - 为了减少 JS 代码包,应当将组件树中的存在交互逻辑的客户端组件单独拆分出来,使得其余部分尽可能成为服务端组件。
  • 从服务端传递给客户端的参数应当可序列化 。

两端代码嵌套

在客户端代码中,依然可以嵌套服务端代码,但有一些注意点:

  • 当你的服务端代码换为客户端代码时,客户端如果要获取数据或资源是需要单独向服务器请求的。
  • 当服务端接收到请求时,所有服务端组件(包括内嵌在客户端组件的服务端组件)均会被最先渲染。渲染结果(RSC Payload)会包含客户端组件的位置信息。在客户端上,React 会根据 RSC Payload 来将服务端和客户端组件合为一棵树。
  • 因为客户端组件会在服务端组件之后渲染,客户端组件代码中不能包含对服务端代码的 import 导入。服务端组件应当作为参数传递给客户端:
    import ClientComponent from './client-component'
    import ServerComponent from './server-component'
    
    export default function Page() {
        return (
            <ClientComponent>
                {/* 该组件依然会在服务端中预先渲染 */}
                <ServerComponent />
            </ClientComponent>
        )
    }
    

Edge 和 Node.js 运行时

TODO

参考

Rendering - nextjs.org

TALAXY 落于 2023年12月19日 。