Next.js 与 Contentlayer:优化 SSR 大数据量
2023-12-23 • ☕️☕️ 9 min read
我的博客使用 Next.js 作为前端框架和 Contentlayer 作为管理内容的工具。Contentlayer 可以使用本地文件作为数据源,这意味着我可以把博客项目代码中与博客文章(Markdown 或 MDX 文件)放在一起。
作为开发者,我对这种内容与代码组合在一起的方式感到很愉快,利用 MDX 可以在文章中嵌入具有交互性的组件,这本身就是网站代码与其内容的结合,内容也被视为组件。
但是到目前为止 Contentlayer 还处于 Beta 阶段,在使用的过程中还是会有些问题。这篇博客我打算从使用 Contentlayer 遇到的问题来说下 Next.js 的一些性能优化的问题。
技术更新迭代过快,下面列出我遇到问题时 Next.js 和 Contentlayer 对应的依赖版本,方便使用相同技术栈的读者排查原因。
"dependencies": {
"next": "13.4.13",
"contentlayer": "0.3.4",
"next-contentlayer": "0.3.4",
}起因
随着博客文章越来越多,在博客首页调试页面时,终端开始出现如下警告。
Warning: data for page "/" is xx MB which exceeds the threshold of 128 kB, this amount of data can reduce performance. See more info here: https://nextjs.org/docs/messages/large-page-data
这是 Next.js 的警告,按提示点进链接,里面分析问题原因是因为我的页面数据太多,让我把从 getStaticProps 、 getServerSideProps 或 getInitialProps 返回的数据量减少到仅呈现页面的基本数据。并让我在浏览器控制台执行以下命令检查传递到页面的 props。
JSON.parse(document.getElementById("__NEXT_DATA__").textContent)可以观察到上面那段代码的作用是从 HTML 中 id 为 __NEXT_DATA__ 的元素中获取的内容。JSON 嵌入在 id 为 __NEXT_DATA__ 的脚本标签内:
<script id='__NEXT_DATA__' type='application/json'>
// JSON here
</script>在执行完这条命令后,仔细观察打印的数据,发现页面的 props 中包含了 Contentlayer 直接返回的数据,每篇文章的 body 字段都包含了文章的原始数据和经由 MDX 插件处理后的代码。
而且这些数据被处理成 JSON 被包含在了页面的 HTML 之中,这意味着 HTML 本身的 size 也会变大。
现在是遇到了两个问题:
- Contentlayer 为什么把文章原始数据都返回了?
- Next.js 为什么会把数据给塞到 HTML 上?
分析
对于第一个问题,我仔细的查看了下 Contentlayer 文档上的示例,发现文档上是直接把生成的 allPosts 给当作页面的 props,而 allPosts 中包含了所有文章的原始数据等信息。
// 引入 contentlayer 生成的所有文章数据
import { allPosts } from 'contentlayer/generated';
/** 省略部分代码... */
export default function Home({ posts }) {
return (
{posts.map((post, idx) => (
<PostCard key={idx} {...post} />
))}
)
}
export async function getStaticProps() {
return {
props: {
posts: allPosts // allPosts 直接作为 props
},
};
}找到原因并且得出一个教训,按着快速开始文档确实可以快速开始,但是还是要根据实际的使用场景对代码进行修改。
再去查看 Contentlayer 的转换数据源所用的 @contentlayer/source-files 文档,看能不能自定义返回的数据,然而翻了一圈后也没找到可以把 body 字段给去掉的配置。最后在 Contentlayer 的 issue 中翻到一个关于 MDX 内容超出 vercel edge function 大小 的问题。
参考里面提出的思路,在内容层没有更好的方法支持的情况下,只好把页面数据进行预处理后再传递给 props。如果是 Next.js 的 page 路由且使用静态站点生成 (SSG) 的渲染模式,在 Next.js 中解决该问题的大致思路:
import { allPosts } from 'contentlayer/generated';
/** 省略部分代码... */
export default function Home({ posts }) {
return (
{posts.map((post, idx) => (
<PostCard key={idx} {...post} />
))}
)
}
export async function getStaticProps() {
// 在传递给页面之前预处理
const posts = allPosts.map((post) => {
const { body, ...content } = post;
return content;
});
return {
props: {
posts
},
};
}深入
在将传递给页面的 props 在 getStaticProps 中进行预处理后,Warning 已经消除,但是此时 HTML 中仍然包含着 JSON 数据。这意味着页面的所有内容都会被提供两次,这可能会让刚接触 Next.js 的同学感到很奇怪。
发生这种情况是因为服务端渲染在 React 和 Next.js 中的工作方式。
这里所说的服务端渲染是广义的,一般来说,传统的“服务端渲染”响应请求而实时发生在实时生产服务器上。而 SSG 的编译时渲染则发生得更早,作为构建过程的一部分。因为都是发生在服务端,所以这些不同的渲染模式有时都被称为服务端渲染。
-
在构建阶段,Next.js 调用每个页面的
getStaticProps函数,该函数在服务器上运行,它的职责是获取构建页面所需的数据。 -
React 得到了数据,现在它在服务器中开始发挥作用,它根据数据构建好一份完整且填充好数据的 HTML 文档。
-
当页面请求到达服务器时,服务器将提前构建好的 HTML 文档返回。
-
当客户端收到 HTML 时,完整的 HTML 立即呈现在客户端。与此同时,客户端上开始加载并执行整个应用程序的 JavaScript 代码,其中包括 React 的 JavaScript。
-
当 React 的 JavaScript 加载完成后,React 在客户端再次开始运行。
-
当 React 再次开始运行时,它需要在服务器上使用相同的数据(回顾 1),Next.js 通过将 JSON 插入到初始 HTML 中 id 为 NEXT_DATA 的脚本标记内,客户端 React 就可以直接使用此数据。
上面总结的是 SSG 的渲染流程,客户端收到的是一份提前编译好的 HTML 文档,但是也仅仅是一份只有内容的文档。如果用户还需要和文档进行交互,那么还需要运行客户端 JavaScript。
客户端 JavaScript 包含用于在编译时生成它的相同 React 代码,此时 React 在客户端上运行。React 开始将 JavaScript 逻辑连接到服务器生成的 HTML 并附加事件侦听器处理用户交互,这个过程被称为水合作用(Hydration)。
React 为了正确地水合(hydrate)并随后渲染 React tree(虚拟 DOM),服务器端可用的相同 props 也必须在客户端可用。而 Next.js 的解决方案是让 React 从 id 为 __NEXT_DATA__ 的元素中获取数据,从而减少客户端的请求。
总结
在本文中,我们探讨了 Next.js 和 Contentlayer 在构建博客时遇到的性能挑战。通过分析大量数据导致的性能警告,我们定位了问题并调整了数据传递方式。我们了解了服务器端渲染和客户端数据水合的工作方式,并从中学到了如何优化数据传递和页面加载性能的经验教训。这个过程不仅解决了问题,也提供了更多优化前端性能的思路。
文章作者:codeep
文章链接:https://blog.codeep.xyz/posts/contentlayer-next-data[Copy]
本文采用 署名 4.0 国际(CC BY 4.0) 许可协议进行许可