讲清楚 Next.js 里的 CSR, SSR, SSG 和 ISR

🧑‍💻
推荐全栈学习资源:
  • Next.js 中文文档:样式和官网一样的中文文档,创造沉浸式Next.js中文学习体验。
  • 《Chrome插件全栈开发》:真实出海项目的实战教学课,讲解Chrome插件和Next.js端的全栈开发,帮助你半个月内成为全栈出海工程师。
  • 在 Web 前端的圈子里,渲染是一个无法绕开的概念。渲染决定了用户能够看到什么,以及他们能多快看到。但是,所有的渲染不都是相同的。随着现代前端开发的演进,我们有了多种不同的渲染方式,每种都有其独特的优势和挑战。

    Next.js 作为 React 的上层框架,为开发者提供了一系列强大的渲染方式——从传统的客户端渲染(CSR)到服务器端渲染(SSR),再到静态网站生成(SSG)和最新的增量静态生成(ISR):每一种方法都有其适用的场景。

    思考一下,为什么我们需要这么多的渲染策略?它们之间有什么不同?如何为你的项目选择合适的策略?在本篇文章中,我们将详细探讨这些问题,一起来深入了解 Next.js 的渲染策略。

    客户端渲染 (CSR)

    客户端渲染(CSR)是 React 应用程序的默认渲染策略。在 CSR 中,应用首次渲染会加载一个最小的 HTML 文件,其中包括负责渲染 DOM 的 JavaScript 文件。然后,由浏览器执行 JavaScript,从 API 获取数据并完成渲染。

    Next.js 中在 useEffect() 中请求数据就属于 CSR:

    import React, { useState, useEffect } from 'react'
     
    export function Page() {
      const [data, setData] = useState(null)
     
      useEffect(() => {
        const fetchData = async () => {
          const response = await fetch('https://nextjs.weijunext.com/data')
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`)
          }
          const result = await response.json()
          setData(result)
        }
     
        fetchData().catch((e) => {
          // handle the error as needed
          console.error('An error occurred while fetching the data: ', e)
        })
      }, [])
     
      return <p>{data ? `Your data: ${data}` : 'Loading...'}</p>
    }

    这个示例中,在请求完成前会显示Loading,请求完成后就会把请求结果渲染到页面。

    优缺点

    优点:

    1. 服务器负载较轻,因为大部分工作都在客户端完成。
    2. 适用于高度交互的应用,如 SPA (单页应用)。
    3. 一旦页面加载完成,页面间的导航和互动会非常迅速。

    缺点:

    1. 首次加载时间可能较长,因为需要下载、解析和执行大量 JavaScript。
    2. 不利于 SEO,因为搜索引擎可能只看到空的 HTML,而不是实际内容。
    3. 增加了客户端的计算负担,可能导致手机等低功耗设备的性能问题。

    服务器端渲染 (SSR)

    Next.js 中,要使用服务器端渲染,需要导出一个名为 getServerSideProps 的异步函数。服务器将在每次请求页面时优先调用该函数。

    例如,假设页面需要预渲染频繁更新的数据(如下截图的页面访问量)。那你就可以编写 getServerSideProps 来获取这些数据并将其传递给页面

    截图自:https://nextjs.weijunext.com

    export default function Page({ data }) {
      // Render data...
     
    	<div>{data.view}</div>
    }
     
    // This gets called on every request
    export async function getServerSideProps() {
      // Fetch data from API
      const res = await fetch(`/api/view`)
      const data = await res.json()
     
      // Pass data to the page via props
      return { props: { data } }
    }

    这个示例中,页面访问量的数据请求无法再浏览器控制台看到,因为getServerSideProps是在服务端执行的,页面渲染的时候就可以马上拿到访问量数据,不会像 CSR 那样有延迟。

    优缺点

    优点:

    1. 首次加载速度快,因为浏览器立即获得完整的页面内容。
    2. 有助于 SEO,因为搜索引擎可以直接爬取和索引完整的页面内容。
    3. 减轻了客户端的计算负担。

    缺点:

    1. 服务器压力较大,尤其是在高流量情况下。
    2. 总体延迟可能增加,因为每次页面请求都需要服务器处理。
    3. 可能需要额外的服务器资源和优化。

    静态网站生成 (SSG)

    如果页面静态网站生成 (SSG),页面 HTML 将在 build 时生成。也就是说,如果你的页面已经发布到生产,这时想修改页面内容,只能重新 build 来完成更新。

    Next.js 支持在 build 时生成带数据或者不带数据的 HTML,来看示例,

    不带数据的静态页面

    function About() {
      return <div>About</div>
    }
     
    export default About

    带数据的静态页面

    带数据的静态页面又区分为两种,一种是页面内容依赖数据,一种是页面路径依赖数据。

    页面内容依赖数据

    页面内容依赖数据,可以使用getStaticProps完成构建时的数据拉取

    export default function Blog({ posts }) {
      return (
        <ul>
          {posts.map((post) => (
            <li>{post.title}</li>
          ))}
        </ul>
      )
    }
     
    // This function gets called at build time
    export async function getStaticProps() {
      // Call an external API endpoint to get posts
      const res = await fetch('https://.../posts')
      const posts = await res.json()
     
      // By returning { props: { posts } }, the Blog component
      // will receive `posts` as a prop at build time
      return {
        props: {
          posts,
        },
      }
    }

    只要页面内使用了getStaticProps,那么 Next.js 都将在 build 时调用并获取数据,然后把数据传给客户端(即 Blog 组件),最后把客户端代码打包成 HTML。

    页面路径依赖数据

    页面路径依赖数据,要同时使用getStaticPropsgetStaticPaths完成构建时的数据拉取。

    假设你创建了一个动态路由文件 pages/posts/[id].js,路由会根据 id 显示博客文章,而这个 id 是什么是由服务端告知客户端的。

    getStaticPropsgetStaticPaths是这样联合使用的:

    export default function Post({ post }) {
      // Render post...
    }
     
    // This function gets called at build time
    export async function getStaticPaths() {
      // Call an external API endpoint to get posts
      const res = await fetch('https://.../posts')
      const posts = await res.json()
     
      // Get the paths we want to pre-render based on posts
      const paths = posts.map((post) => ({
        params: { id: post.id },
      }))
     
      // We'll pre-render only these paths at build time.
      // { fallback: false } means other routes should 404.
      return { paths, fallback: false }
    }
     
    // This also gets called at build time
    export async function getStaticProps({ params }) {
      // params contains the post `id`.
      // If the route is like /posts/1, then params.id is 1
      const res = await fetch(`https://.../posts/${params.id}`)
      const post = await res.json()
     
      // Pass post data to the page via props
      return { props: { post } }
    }

    我们首先要使用getStaticPaths来获取需要预渲染的路径,然后再使用getStaticProps获取带有此 id 的博客文章,这样 build 时就能完成依次调用getStaticPathsgetStaticProps来实现动态路由的静态页面渲染。

    SSG 无疑是几种渲染方式里最快的,所以你应该在较少变动数据的页面尽量使用 SSG。

    优缺点

    优点:

    1. 极快的加载速度,因为服务器仅提供预生成的文件。
    2. 减轻了服务器压力,因为不需要实时渲染。
    3. 非常适合内容不经常变动的网站或应用。

    缺点:

    1. 不适合内容经常变动或需要实时更新的应用。
    2. 需要在内容变更时重新生成所有页面。
    3. 可能需要额外的构建和部署步骤。

    增量静态生成 (ISR)

    增量静态再生(ISR)建立在 SSG 的基础上,同时又有 SSR 的优点,ISR 允许页面的某些部分是静态的,而其他部分则可以在数据发生变化时动态渲染。ISR 在性能和内容更新之间取得了平衡,因此适用于内容经常更新的站点。

    先来看一个示例:

    function Blog({ posts }) {
      return (
        <ul>
          {posts.map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      )
    }
     
    // This function gets called at build time on server-side.
    // It may be called again, on a serverless function, if
    // revalidation is enabled and a new request comes in
    export async function getStaticProps() {
      const res = await fetch('https://.../posts')
      const posts = await res.json()
     
      return {
        props: {
          posts,
        },
        // Next.js will attempt to re-generate the page:
        // - When a request comes in
        // - At most once every 60 seconds
        revalidate: 60, // In seconds
      }
    }
     
    // This function gets called at build time on server-side.
    // It may be called again, on a serverless function, if
    // the path has not been generated.
    export async function getStaticPaths() {
      const res = await fetch('https://.../posts')
      const posts = await res.json()
     
      // Get the paths we want to pre-render based on posts
      const paths = posts.map((post) => ({
        params: { id: post.id },
      }))
     
      // We'll pre-render only these paths at build time.
      // { fallback: 'blocking' } will server-render pages
      // on-demand if the path doesn't exist.
      return { paths, fallback: 'blocking' }
    }
     
    export default Blog

    这个示例和 SSG 的示例大同小异,为什么能做到增量渲染呢?核心就在于revalidatefallback

    当我们使用 revalidate选项时,Next.js 会在 build 时调用一次getStaticProps,部署生产后,Next.js 还会在达到revalidate设置的时间间隔后再次运行getStaticProps,以此更新内容。

    fallback则是用来决定当用户请求一个在构建时未被预渲染的路径时,Next.js 应当怎么处理。它有三种可选值:falsetrue 和 **'**blocking**'**

    1. fallback: false:
      • 当用户请求一个在构建时未被预渲染的路径时,将立即返回 404 页面。
      • 这意味着如果路径不在getStaticPaths返回的列表中,用户会看到一个 404 错误。
    2. fallback: true:
      • 当用户请求一个未被预渲染的路径时,Next.js 会立即提供一个“fallback”版本的页面。这通常是一个空页面或一个加载状态。
      • 然后,Next.js 会在后台异步地运行getStaticProps来获取页面的数据,并重新渲染页面。一旦页面准备好,它将替换“fallback”版本。
      • 这允许页面几乎立即可用,但可能不显示任何实际内容,直到数据被加载并页面被渲染。
    3. fallback: 'blocking':
      • 当用户请求一个未被预渲染的路径时,Next.js 会等待getStaticProps完成并生成该页面,然后再提供给用户。
      • 这意味着用户会等待,直到页面准备好,但他们会立即看到完整的页面内容,而不是一个空页面或加载状态。

    优缺点

    优点:

    1. 结合了 SSR 的实时性和 SSG 的速度优势。
    2. 适合内容经常变动但不需要实时更新的应用。
    3. 减轻了服务器的压力,同时提供了实时内容。

    缺点:

    1. 相比于 SSG,初次请求可能需要更长的加载时间。

    如何选择合适的渲染策略建议

    1. 高度交互的应用:如果你正在开发一个如单页应用(SPA)那样高度交互的应用,CSR 可能是最佳选择。一旦页面加载,用户的任何交互都将非常迅速,无需再次从服务器加载内容。
    2. 需要 SEO 优化的应用:如果你的应用依赖于搜索引擎优化,SSRSSG 是更好的选择。这两种方法都会提供完整的 HTML,有助于搜索引擎索引。
    3. 内容静态但更新频繁的网站:例如博客或新闻网站,ISR 是一个很好的选择。它允许内容在背景中更新,而用户仍然可以快速访问页面。
    4. 内容基本不变的网站:对于内容很少或根本不更改的网站,SSG 是最佳选择。一次生成,无需再次渲染,提供了最快的加载速度。
    5. 混合内容的应用:Next.js 允许你在同一个应用中混合使用不同的渲染策略。例如,你可以使用 SSR 渲染首页,使用 SSG 渲染博客部分,而使用 CSR 渲染用户交互部分。

    结语

    CSR、SSR、SSG、ISR 这些看起来让人头疼的概念,实际上都有适合自己的场景,只要分析场景,结合文档使用就不会再迷茫。

    专栏资源

    专栏介绍:以实战的角度进行Next.js生态圈的技术栈分享,内容包括但不限于:Next.js理论知识、功能模块设计思路、实战中使用到的技术栈。这是一个长期更新的专栏,我会持续把自己的思考和经验提炼分享出来,欢迎关注我的专栏👇

    专栏地址:👉Next.js生态圈实战

    专栏演示站:👉Next.js Demos

    专栏源码仓库:👉Github - Source Code

    交个朋友:👉加入「独立全栈交流群」