混合渲染模式

混合渲染是NextJS框架最重要的特性。在传统的CSR(Client Side Rendering)工程中,整个UI都由浏览器执行JavaScript代码渲染,但CSR存在首屏展现慢等问题,于是出现了SSR(Server Side Rendering)技术。除此之外,对于一些固定的页面,可以采取SSG(Static Site Generation)方式直接生成静态页面,这样能直接跳过请求服务端接口的时间,并充分利用CDN和缓存机制。NextJS13实现了混合渲染,框架能够将这些渲染方式结合,自动为我们选择合适的页面渲染方式,互相取长补短,达到最好最快的渲染效果。

静态渲染 vs 动态渲染

静态渲染(SSG):也叫服务端生成(SSG),在工程构建阶段可以直接生成HTML静态页面,浏览器请求时直接将HTML页面返回。这类页面可以理解为就是在工程构建阶段一次性生成的。

动态渲染(SSR):也叫服务端渲染(SSR),由于存在一些需要从服务端动态获取的参数,在工程构建阶段无法直接构建一个固定的HTML页面,浏览器请求页面时,会在服务端动态请求数据、计算并输出页面内容。当然,你可以说客户端组件也都是动态渲染,这没有问题,但我们这里主要对比服务端渲染和服务端生成的区别。

NextJS框架会自动判断应该使用静态渲染还是动态渲染,如果页面组件用到了非缓存的请求(如获取数据时指定了{ cache: "no-store" }),NextJS会自动使用动态渲染,否则使用静态渲染。我们在NextJS的构建阶段通常会看到类似这样的提示:

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)

它代表的就是使用动态渲染和静态渲染的文件。

服务端渲染 vs 客户端渲染

服务端渲染(SSG+SSR):HTML由服务器端计算并返回给浏览器,前面介绍的服务端生成(SSG)和服务端渲染(SSR)都属于此类。服务端组件有许多优势,它具备完整的服务端能力,在服务端执行在某些条件下能极大改善页面加载完成的时间,此外还能减少JavaScript打包后的大小。

客户端渲染(CSR):服务器端只返回基本的HTML和JavaScript代码,UI界面由浏览器计算生成,传统意义上的前端如CRA、UmiJS等框架都属于此类。NextJS默认会使用服务端渲染来将组件渲染为HTML代码,但使用到DOM或是BOM接口的操作是无法在服务端执行的,此时就需要使用"use client"标注组件为客户端组件,客户端组件的内容和数据都是在浏览器端处理的,并且可以利用客户端浏览器的事件和状态。

下面代码是一个计数器的例子,它用到了浏览器相关的功能,因此必须声明为客户端组件。

"use client";

import { useState } from "react";

const Page = () => {
  const [cnt, setCnt] = useState(0);

  return (
    <>
      <button
        onClick={() => {
          setCnt(cnt + 1);
        }}
      >
        {cnt}
      </button>
    </>
  );
};

export default Page;

上面代码我们标注了"use client",它可以理解为混合渲染组件树中,服务端组件和客户端组件的边界。从组件树的根部开始,默认都是服务端组件,一旦某个组件使用了"use client"的组件,它和其子组件都是客户端组件。

混合渲染的组件树

NextJS中服务端组件(SSR、SSG)和客户端组件(CSR)的组件都是混合在一起的,它们结合使用才能发挥出最大的优势。通过上面学习我们可以发现,使用了"use client"的组件,它和其子组件都是客户端组件。这是显而易见的,客户端组件应该放在组件树的叶节点,以最大程度的减小客户端包的体积,加快页面首屏打开速度。

但这又会产生一个问题,如果我需要在组件树根部使用Context Provider,它仅能用于客户端组件,我们又不可能将整个组件树都改为客户端渲染,此时该怎么办?下面是一个例子。

layout.jsx

"use client";

import React from "react";

export const UserContext = React.createContext({});

const DashboardLayout = ({ children }) => {
  return (
    <UserContext.Provider value={{ username: "tom" }}>
      <html lang="en">
        <body>{children}</body>
      </html>
    </UserContext.Provider>
  );
};

export default DashboardLayout;

page.jsx

import CompA from "./_components/CompA";

const Page = () => {
  return <CompA></CompA>;
};

export default Page;

_components/CompA/index.jsx

"use client";

import { useContext } from "react";
import { UserContext } from "@/app/dashboard/layout";

const CompA = () => {
  const { username } = useContext(UserContext);

  return <div>{username}</div>;
};

export default CompA;

实际上这里的layout.jsx中,只是用children提供了一个插槽,它是将服务端组件作为一个属性向下传递的,我们将Context Provider放在这里是合理的。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。