Next.js logomarkNext.js 14 ISR+SSG

2024年5月20日

Openai logomark
Hamster1963

前言

现在你正在浏览的这个站点采用 Next.js14 搭配 ISR 与 SSG 进行搭建,针对首页与其他页采用不同的渲染方式,其中首页采用 ISR 的方式来同步音乐信息,在博客页面采用 SSG 的方式来预渲染博客列表与内容,在访客页面采用 SSR 的方式来实时获取访客的留言。

可以说使用这些不同的渲染方式最终的目的都是为了极致优化用户体验,让网站更快,更环保。(逃

speed.png
PageSpeed

在一个框架中采用如此丰富的渲染方式,一方面体现了框架的灵活性,可以针对不同的场景进行适配,但同时,如此多样的方式也给开发者带来不小的挑战,因此这篇博客就以搭建一个博客站点为例子,尝试将这些渲染方式应用在不同的场景中。

首页-ISR

ISR(Incremental Static Regeneration),在 Next.js 的 Pages Router 文档中,有对 ISR 详细的介绍与使用方式,现在关于 App Router 的 ISR 先让我们留一个悬念给后文。

Data Fetching: Incremental Static Regeneration (ISR)

最关键的使用方式便是将**revalidate** 参数添加到 getStaticProps 方法中,

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 10 seconds
    revalidate: 10, // In seconds
  }
}

function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

getStaticProps 方法中定义了**revalidate** 后,当 Next.js 服务端接收到新的请求后会重新构建页面,且在构建完成后会将构建(渲染)结果保存在缓存中,而对于后续的请求,如果在设置的缓存时间间隔内,则直接获取缓存中渲染好的页面并返回。(如下图)

Untitled.png

这么做是有许多好处的,首先最显而易见的便是在缓存时间间隔内,相对于 SSR 每次进行渲染,ISR 明显降低了渲染页面的次数,并且是以近乎静态页面的方式去响应请求。这不仅可以缓解一些重计算的压力(读取数据库,调用第三方接口),同时也使得页面的响应速度大幅增加。

在了解了基础的原理后,让我们把目光回到 Next.js14+App Router 中,在 App Router 中,官方文档并没有给出特别明确的 ISR 配置文档,相反在 App Router 中则是采用缓存优先的方式去接管每一个 服务端的 fetch 请求,默认状态下都缓存了结果,而如果需要每次都是动态请求则需要手动配置 revalidate 为 0 或者 cache 为 dynamic 的方式来绕过缓存,因此在这种情况下 ISR 似乎成为了默认的配置,但如果我们不通过官方带有缓存行为的方式去获取数据,比如 kv,或者手动通过 ioredis 去获取数据,那配置 ISR 则需要一些特殊的方式。

在 Next.js 14 中,官方介绍了一种新的 cache 组件以供开发者手动去缓存所需的数据。

Functions: unstable_cache

unstable_cache allows you to cache the results of expensive operations, like database queries, and reuse them across multiple requests.

可见unstable_cache 实际上就是将 ISR 中的缓存部分抽离出来,为开发者提供一种全局缓存的方式去缓存数据。

前文我们提到,ISR 是以近乎静态页面的速度来响应请求,是用近乎则是因为实际上在获取页面的过程中,仍需要通过读取缓存的方式去获取已经渲染完成的页面,只不过在 vercel 优秀的全球网络架构下,这种延迟被优化到近乎可以忽略。

我们可以通过 vercel 的仪表盘日志来查看这个过程。

Untitled

使用unstable_cache 十分简单,将需要缓存的结果包裹起来即可。

import { unstable_cache as cache } from 'next/cache'
import { kv } from '@vercel/kv'

const cacheGetNowPlaying = cache(
  async () => {
    return (await kv.get) < any > 'live:mac-music-now'
  },
  ['mac-music-now'],
  {
    revalidate: 10,
    tags: ['music'],
  }
)

同时在第三个参数中可以针对 revalidate 与 tags(缓存键)进行更加细化的配置。

使用 cache 后的数据也十分简单。

export default async function NowPlaying() {
  const data = await cacheGetNowPlaying();
  return ...
}

这下,ISR 便在 App Router 中配置好了,但同时目前的配置也带来了不可忽视的问题:

  1. 在缓存时间间隔内,无论源数据是否有更新都无法触发新的页面渲染。
  2. 在缓存过期后,第一个触发 ISR 的请求会因为触发服务器重新渲染的原因,而处于很长的等待服务器时间,造成不好的用户体验。

因此可以采用手动进行缓存过期管理与 ISR 预热的方式来优化用户体验。

const cacheGetNowPlaying = cache(
  async () => {
    return (await kv.get) < any > 'live:mac-music-now'
  },
  ['mac-music-now'],
  {
    revalidate: false,
    tags: ['music'],
  }
)

在配置项中,将 revalidate 设为 false,这会将缓存的时间设置为无限长(其实大概是 1 年),同时在 tags 中配置一个独一无二属于这个缓存的 key,这两部分的配置可以解释为不进行缓存过期,只有通过某种方式才可以触发缓存过期。而这种方式就是通过 next/cache 中的revalidateTag 来针对 tag 进行缓存过期。

因此可以在源数据更新后通过触发 App Router 中定义的 API 来进行缓存过期。

import { revalidateTag } from 'next/cache';

export async function POST(req: NextRequest) {
  try {
    revalidateTag('music');
    return NextResponse.json({ message: 'success' }, { status: 201 });
  } catch (error) {
    return NextResponse.json({ error }, { status: 400 });
  }
}

那缓存刷新也十分简单,只需要在触发缓存过期后直接请求对应的站点,Next.js 就会重新构建页面并缓存。

以 Rust 为例。

trace!("开始触发 ISR");
let isr_url = "https://buycoffee.top/";
let client = Client::new();
let response = client
    .get(isr_url)
    .header("User-Agent", "rust-cron-app")
    .send()
    .await?;
if response.status().is_success() {
    info!("触发 ISR 成功: {}", response.status());
    } else {
    info!("触发 ISR 失败: {}", response.status());
}

CleanShot 2024-05-20 at 23.05.24@2x.png

通过这种手动控制的方式,不仅可以避免了 ISR 中初次访问较慢的问题,同时也可以更加精确地控制缓存。搭配 loading.tsx 的使用,更是可以使得用户可以无感切换到新构建页面而无需等待加载阻塞。

Untitled

这是一个切换的 Demo。

博客页-SSG

而对于一些不太依赖动态数据,且布局一致的动态路径,SSG 便可以派上用场了。

SSG**(Static Site Generation)**

Rendering: Static Site Generation (SSG)

以站点中的博客为例,博客的路径是动态的 [slug]:

Untitled

但博客的内容实际上都是静态存储在文件中,通过 mdx 渲染成 HTML 的。

ray-so-export.png

如果不使用 SSG 在编译阶段对 mdx 进行静态渲染,则 Next.js 会在接收到请求后再进行服务端渲染,而对于这种几乎不会变更的内容,使用 SSG 在构建阶段渲染则可以将动态的路径预先渲染出来,在用户请求时就可以直接返回静态页面。

在 Next14 App Router 中,对于静态路径,使用*generateStaticParams* 方法便可以使用 SSG。

export async function generateStaticParams() {
  let getPost = getBlogPosts();
  getPost = getPost.filter((post) => post.metadata.category !== 'Daily');
  return getPost.map((post) => ({
    slug: post.slug,
  }));
}
export default async function Page({ params }) {
  const { slug } = params;
  return (
    <section className="sm:px-14 sm:pt-6">
      <BlogContent slug={slug} />
    </section>
  );
}

generateStaticParams 方法中进行全部动态路径的获取,并将其整体作为返回值,在页面中使用 params 参数进行路径的接收与使用,通过这种方式,Next.js 便会在构建时执行方法并渲染获取到的路径内页面。

ray-so-export (2).png

访客页-SSR

在访客页面,在每次请求页面时,服务器读取数据库获取最新的留言进行显示,因此采用 SSR 搭配 loading.tsx 的方式进行构建。

ray-so-export (4).png

SSR 大家都不陌生,因此在这就不做过多的介绍,重点是在于如何让用户的等待体验更好,骨架屏往往比一个旋转的图标更好,因此在 Next.js 中可以通过在 page.tsx 路径下增加 loading.tsx, 来使得页面在 SSR 期间也可以在客户端上显示一些所需的信息。

Routing: Loading UI and Streaming

Untitled

请求页面时,loading 内定义的组件会立即显示在客户端页面上,避免了阻塞等待所带来的糟糕用户体验。

同时,在用户提交了留言后,应使用 revalidatePath 方法使页面重新渲染,以获取最新的留言信息。

ray-so-export (5).png

下面是一个 Demo :

总结

通过在站点中使用不同的渲染方式,不仅可以很好的应对了不同的场景,同时也使得站点具有良好的用户体验,在具体的开发中可以多去尝试不同的方式,感受不同渲染方式所带来的开发体验与用户体验,构建 UI/UX 良好的站点。

Hope u enjoy.

hamster:测试一下新的留言功能...