渲染策略分类
SSR:Server-Side Rendering,服务器端渲染;
SSG :Server-Static Generation,服务端静态生成,也叫编译生成
ISR :Incremental Static Regeneration 静态增量生成
CSR:Client-Side Rendering,客户端渲染
默认情况,Next.js 会预渲染每个页面。这意味着 Next.js 会将网站的所有页面提前生成静态 HTML 文件并保存下来。当用户访问网站时,服务器会返回预渲染好的静态 HTML 文件,而不是使用 JavaScript 动态生成网页内容。
在直接使用 Vue 或者 React 时,页面的渲染是完全由 JS 执行的,在 JS 未渲染完成前,页面中可能只有类似<div id='root'></div>
的一个容器标签,当 JavaScript 文件被加载并执行时,它们会根据代码逻辑和数据来生成最终的 HTML 标记和页面内容,并将其插入到页面中的容器标签中。
在 Next.js 中想要使用某种渲染方式,例如我想指定 SSR 或是 SSG,并不是说直接在某个配置文件中改一个配置就可以的,而是我们在页面中使用了一些相关的 API,而 Next.js 在打包时检测到了才会进行相应的页面构建策略。
渲染效果对比
在 Next.js 中,预渲染(Pre-rendering)和非预渲染(Non-pre-rendering)是两种不同的页面渲染方式
-
预渲染是指在构建时或请求时生成 HTML 页面,并在客户端请求时直接返回生成的 HTML。预渲染可以提高页面加载速度和 SEO 性能。即SSG、SSR。
-
非预渲染是指在客户端渲染页面内容。这意味着 HTML 页面在客户端生成,而不是在构建时或请求时生成。适用于需要在客户端动态生成内容的页面。即CSR。
静态网站生成(SSG)
Next’s 中静态网站生成一般分为三种情况,分别是
- 纯静态页面,不需要依赖外部数据
- 页面的 内容 依赖外部数据
- 页面的 路径 依赖外部数据
不需要依赖外部数据
function About() {
return <div>About</div>
}
export default About
这种情况最简单,Next.js 会在打包时直接生成一个静态的 HTML。
页面的内容依赖外部数据
export default function Blog({ posts }) {
// 渲染文章...
}
// 这个函数会在 build 时接收请求
export async function getStaticProps() {
// 请求 API 获取文章数据
const res = await fetch('https://.../posts')
const posts = await res.json()
// 在构建时,Blog 组件可以接收到 posts 这个 props
return {
props: {
posts,
},
}
}
为了能够在预渲染的时候拿到外部的数据,可以在定义组件的那个文件中暴露一个异步函数 getStaticProps
,在打包页面时 Next.js 会先执行 getStaticProps
中的操作,例如发送请求,数据处理之类的。
然后可以返回一个对象,其中 props
字段的值会在渲染组件时作为 props
传入,这样就实现了构建时从外部获取数据。
页面的路径依赖外部数据
Next.js 的路由是由文件系统驱动的,每个页面都对应着一个文件,这个文件的路径就是这个页面的路由路径。例如,pages/index.js
对应着根路径 /
,而 pages/about.js
对应着 /about
路径。
当然不是所有的页面都是固定路由的,而动态路由就是一种可以基于 URL 参数动态生成页面的路由。例如,你可以创建一个 /posts/[id]
的页面路由,用于显示具有不同 id
值的文章详情页面。
而如果我们使用了动态路由,例如前面提到的文章 id
,那么 Next.js 如何得到我们有哪些文章 id
以实现预渲染呢?
// 这个函数在 build 时会调用
export async function getStaticPaths() {
// 请求 API 获取数据
const res = await fetch('https://.../posts')
const posts = await res.json()
// 基于 posts 获取我们想要预渲染的路径
const paths = posts.map((post) => ({
params: { id: post.id },
}))
// 在 build 时我们只会预渲染 paths 数组中的路径
// { fallback: false } 意为其他的路由都会返回 404
return { paths, fallback: false }
}
可以在定义组件的那个文件中再暴露一个异步函数 getStaticPaths
,这个函数可以返回一个包含 paths
和 fallback
属性的对象,paths
是一个包含了所有动态路由路径的数组,它决定了哪些路径将被预渲染。而 fallback
则表示访问还没有生成 html 的路径时,要采取怎么样的策略,它存在三种情况
false
:访问任何在getStaticPaths()
函数中未返回的路径都会响应 404 页面。true
:build 时未生成的路径不会返回 404 页面。而是在第一次请求时进行 SSR 并返回生成的 HTML。这个生成的 HTML 会被缓存,也就是说你访问这个路径时,即便对应的数据已经更新了还是会返回原本的页面,直到缓存过期重新生成 HTML。'blocking'
:'blocking'
策略与设置为true
时相同,不同点在于设置为true
时,我们会直接进入访问的路径,并同时执行getStaticProps
,并且可以通过router.isFallback
判断getStaticProps
是否已经执行完成,而'blocking'
策略则会先执行getStaticProps
,直到执行完成才会进入新的页面。也就是同步与异步的关系。
虽然预渲染这么方便,将我的每个页面都生成静态文件了,那如果页面更新了怎么办呢?难道我需要执行 build 重新生成所有页面嘛?这里就需要引入 ISR 的概念了。
静态增量生成(ISR)
ISR(Incremental Static Regeneration) 是一种在静态站点生成过程中,增量更新部分页面的技术。传统的静态网站生成方式需要每次完全重新生成全部页面,这在网站内容变化频繁的情况下可能会导致生成时间较长,同时也可能会降低网站的性能。
ISR 的思想是只 重新生成需要更新部分页面。这样可以显著减少生成时间和资源占用,并且能够提高网站的响应速度。当用户请求一个需要更新的页面时,ISR 会在后台自动重新生成该页面,然后将其缓存,以便下一次请求时可以快速响应。
要在 Next.js 中开启 ISR ,只需要在前面介绍的 getStaticProps
函数中返回一个 revalidate
属性,下面放一个官方的示例:
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
// 这个方法会在服务端渲染或者 build 时被调用
// 当使用了 serverless 函数、开启 revalidate 并且接受到新的请求时也会被重新调用
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
// Next 将会尝试重新生成页面:
// - 接受到新的请求
// - 每隔最多十秒钟
revalidate: 10, // 单位为秒
}
}
// 这个方法会在服务端渲染或者 build 时被调用
// 当该路径还没有被生成过就会被重新调用
export async function getStaticPaths() {
const res = await fetch('https://.../posts')
const posts = await res.json()
// 基于 posts 获取我们想要预渲染的路径
const paths = posts.map((post) => ({
params: { id: post.id },
}))
// 在 build 时,我们将只预渲染 paths 中的路径
// { fallback: 'blocking' } 在页面不存在时按需进行服务端渲染。
return { paths, fallback: 'blocking' }
}
export default Blog
ISR 的实现方式是将某些页面标记为可 ISR 页面。这些页面在生成时与 SSG 页面相同,但它们还有一个“revalidate”(再生成时间) 。这个时间告诉 Next.js 何时需要重新生成页面。
例如,假设我们需要构建一个新闻网站,希望能在首页上显示最新的新闻文章。在 Next.js 中,我们就可以将首页标记为 ISR 页面,并设置重新生成时间为 10 秒钟。这意味着,Next.js 将在访问首页时,返回上一次生成的静态页面,同时在后台开始重新生成新的页面。10 秒后,当新页面生成完毕后,Next.js 会将新页面替换旧页面,并提供给用户。
服务端渲染 (SSR)
在 Next.js 中,如果启用了 SSR,那么每次的每次请求都会重新生成页面。想要开启 SSR,我们可以在定义组件的那个文件中暴露一个异步函数 getServerSideProps
,这个方法类似于 getStaticProps
,只是执行的时机不同, getServerSideProps
会在每次页面接受请求时都调用。
export default function Page({ data }) {
// 渲染数据...
}
// 这个方法每次请求时都会调用
export async function getServerSideProps() {
// 从外部 API 获取数据
const res = await fetch(`https://.../data`)
const data = await res.json()
// 通过 props 向组件内传入数据
return { props: { data } }
}
除了调用时机外,getServerSideProps
与 getStaticProps
并无二致。
客户端渲染 (CSR)
可以使用 Next.js 的 客户端渲染 + next export 实现。
在 Next.js 中想要使用客户端渲染也很简单,只要上述的这些 API ,例如 getStaticProps
、getServerSideProps
都没有使用,数据都是通过在组件内部使用 axios 或者 fetch 去发送请求获取并渲染的,那么我们使用的就是纯客户端渲染了,与直接使用 Vue 或 React 并无二致。
但即便你的每一个页面都是使用的客户端渲染,在执行 build 时,Next.js 还是会为你打包成一个需要持续运行的 Node.js 服务,而不是直接打包为一个静态文件。
想要导出为静态文件,需要使用单独的打包方式,那就是静态 HTML 导出。
静态 HTML 导出
Next.js 可用于生成静态应用程序,可以实现在浏览器中使用 React 而无需 Node.js 服务器。就可以使用 next export
命令去将你的站点构建为一个静态的文件。
首先更新 next.config.js
中的 output
为 export
:
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
然后更新 package.json
中 build
指令以包含 next export
:
"scripts": {
"build": "next build && next export"
}
运行 npm run build
会生成一个 out
目录。
如何选择渲染方式
选择客户端渲染(CSR)的场景:
- 交互式用户体验:如果你的应用需要高度交互式的用户体验,比如单页应用程序(SPA)那样的即时反馈和流畅的界面切换,CSR 是一个很好的选择。它允许在不重新加载整个页面的情况下动态更新内容,提升了用户体验。
- 动态数据需求:当应用中的大部分内容依赖于用户操作或实时更新时(如聊天应用、仪表盘、个性化内容展示),CSR 能更高效地处理这些需求,因为它可以直接在客户端根据用户行为或API响应来更新页面。
- 减少服务器负担:CSR将渲染工作转移到了客户端,这意味着服务器只需提供初始的HTML文档和必要的数据API,减少了服务器的CPU和内存消耗,尤其适合流量较大的应用。
- 初始加载时间容忍度:如果用户群体对首次页面加载时间有较高容忍度,或者可以通过骨架屏、懒加载等技术缓解等待感,那么可以选择CSR。因为相比SSR,CSR在首次加载时可能需要更长时间来下载JavaScript bundle并执行渲染。
- SEO非关键或已有解决方案:如果SEO不是项目的主要关注点,或者已经有解决方案可以弥补CSR在SEO上的不足(例如使用预渲染、动态渲染服务),则可以偏向于选择CSR。
选择服务器端渲染(SSR)的场景:
- SEO优化:对于依赖搜索引擎流量的内容驱动型网站,如新闻门户、博客、电商产品列表等,SSR至关重要。搜索引擎爬虫通常更偏好服务器端渲染的页面,因为它们可以直接抓取到完整渲染的HTML内容,有助于提升搜索引擎排名。
- 首屏加载速度:在慢速网络环境下或对首屏加载时间有严格要求的应用中,SSR可以提供即时的、可交互的初始页面内容,无需等待JavaScript下载和执行,从而改善用户体验。
- 减少客户端负担:对于低端设备或资源受限的客户端,SSR可以减轻其计算负担,因为大部分渲染工作在服务器完成,客户端只需处理交互逻辑。