Plumbiu
banner
avatar
Plumbiu
这人很勤奋,啥都没留

博客系统中 Next.js 优化

2025-04-24
Blog

Next.js 是一个很优秀的全站框架,支持 SSR、CSR 和 SSG 等混合渲染,这篇博客就是 CSR 和 SSG 混合渲染的。

SSG

SSG 即静态站点生成,在我们运行 build 命令之后,Next.js 会将页面的所有内容通过预构建生成 HTML,这样用户访问网站内容只需要传输预先构建的 HTML 即可,而无需像 SSR 一样,使用服务器资源动态生成 HTML。

一篇博客的网站完全可以使用 SSG 预构建,因为博客内容在你写完之后基本是一直不变的,没必要每次都通过服务器渲染生成内容。

使用 SSG 渲染在 APP 路由中需要导出 generateStaticParams 函数

app/post/[id]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map((post) => ({
    id: post.title,
  }))
}

避免 "use client"

"use client" 表明该组件将在客户端进行渲染,一般用在需要水和的场景,例如为 DOM 元素绑定点击事件等。

这里说避免 use client 并不是完全不适用这个指令,毕竟绑定事件肯定无法在服务端中运行,主要想表达的是,一些需要在客户端渲染的 HTML 内容,完全可以单独分出一个组件,避免和其他无副作用的组件(服务端组件)混在一起。

事先思考的问题

很多人都有一个想法(包括最初的我),直接在 layout 中写个 "use client",那我就不用考虑服务端和浏览器环境了,这样性能和方便性不就一举两得了?

有上面想法的人可能没清楚 SSR 相比 CSR 的性能优势在哪里,你可能要理解下面这个问题,当我最开始接触 Next.js 的时候,字节面试官问了我这个问题:

  • SSR 为什么比 CSR 快?SSR 一定比比 CSR 快吗?

当初我稀里糊涂的回答,大多数情况 SSR 都比 CSR 快,因为 CSR 需要 JavaScript 动态生成 HTML 内容,所以会有白屏,而 SSR 服务器将渲染好的 HTML 返回给了用户,不会有白屏效果。但是面试官紧接着就问:

  • 那 SSR 也是服务器里的 JavaScript 动态生成 HTML,为啥 SSR 就比 CSR 快了?

好吧,我认输,这一块当初确实还不明白,只说了句跟服务器硬件应能有关吧。

根本原因

其实最根本的原因之一在于,SSR 往往所需要的网络传输小于 CSR,这是因为很多库都可以转移到 SSR 中。例如如果你想要将 markdown 格式转换为 html 格式的话,可能会用到 remark-rehypemarkdown-it 一类的库。

如果你使用 CSR,那么这些库就会打包进最终代码,但如果你使用 SSR,那么这些库代码只会在服务端消耗,不会带进客户端,你可以打开 F12 查看一下网络传输,看一下本篇博客所有文件的大小。

如果加上代码高亮,shikihighlight.js 这种体积往往非常大。

总而言之,SSR 等会将编译文件转移到服务端,从而减少客户端网络传输的文件大小。

图片优化

图片优化确实是绕不过的话题了,幸运的是,使用 react-markdown 的库很容易实现图片优化:

import Markdown from 'react-markdown'
import Image from 'next/image'

<Markdown
  components={{
    img(props) {
      const { src, alt } = props
      return <Image src={src} alt={alt} width={WIDTH} height={HEIGHT} />
    },
  }}
/>

当然,你可以使用 sharp 获取图片大小和 blur-hash 值,这样图片未加载完成时,就不会显示空白了:

import sharp from 'sharp'

export default async function getBlurDataUrl(filePath: string) {
  try {
    const image = sharp(filePath)
    const metadata = await image.metadata()
    const originWidth = metadata.width
    const originHeight = metadata.height
    if (!(originHeight && originWidth)) {
      return {}
    }
    const resizedSize = 14
    const resizedImage = image.resize({
      width: Math.min(originWidth, resizedSize),
      height: Math.min(originHeight, resizedSize),
      fit: 'inside',
    })
    const output = resizedImage.webp({
      quality: 20,
      alphaQuality: 20,
      smartSubsample: true,
    })
    const { data } = await output.toBuffer({ resolveWithObject: true })
    return {
      base64: data.toString('base64'),
      metadata,
    }
  } catch (error) {
    return {}
  }
}

然后使用 MarkdownAsync 生成:

import { MarkdownAsync } from 'react-markdown'
import Image from 'next/image'

<MarkdownAsync
  components={{
    async img(props) {
      const { src, alt } = props
      const imagePath = getAssetImagePath(src)
      const { base64, metadata } = await getBlurDataUrl(
        decodeURIComponent(imagePath),
      )
      const { width, height } = metadata ?? {}
      if (!width || !height || !base64) {
        return props.children
      }
      const imageProps = {
        src: resolveAssetPath(`images/${src}`),
        alt,
        blurDataURL: getBase64Url(base64),
        placeholder: 'blur',
        width,
        height,
      }
      return <Image {...imageProps} />
    },
  }}
/>

一些极端的方式

最小化类名

Next.js 很奇怪,对于 module css,类名的样子大概是 文件相对路径+Hash值+类名,所以如果你的组件嵌套比较深的话,类名往往非常之长,你可以使用 @nimpl/classnames-minifier 生成最小化类名。

NOTE 使用 @nimpl/classnames-minifier 意味着不能使用 turbopack 打包,但是你可以通过 process.env.NODE_ENV 判断开发和生产环境,开发的时候使用 turbopack 即可。

如果使用原子化 CSS,那就不用考虑这个问题了。

减少库的使用

使用库一般稳定性、兼容性和功能很不错,但是代码体积也会上升,这个看衡量吧。如果比较难实现,那我就会使用库了。

先总结一下哪些库是自己写的吧:

  • 一些 UI 组件。
  • markdown 扩展。
  • Overlay scrollbar。
  • 基于 Github issues 的评论。

UI 组件例如 SelectorModal 等,自己写实现的功能并不是很多,大概能用就行,然后还有图片预览是模仿 antd-image 实现的,不过缺少下面的功能栏,这个拖拽计算当时做了两天,很困难。

markdown 扩展可以看这篇自定义组件测试文章,实现了 codesandbox 等库的功能。

其次这篇博客的页面滚动条,是模仿 overlayScrollbars 实现的,自己的代码在 github 仓库,加上 css 不到 200 行代码。

评论实现的话,其实有很多库使用都很方便,加一个 script 标签就行了,但是颜色主题不好控制。所以自己用 Github APP 搓了一个,大家想要自己实现的话,可以看阮一峰老师 Github OAuth 这篇文章。

特别难的也有,比如说 image-gallery,效果在这里 测试文章#image-gallery,计算起来很困难,比那个图片预览感觉还要难计算,所以就直接用 react-photo-album 这个库了。

shiki 的优化

简单来说,shiki 的代码是内联样式,会导致很多重复的 style 属性,可以看这篇文章 转换 Shiki 的内联样式 了解如何优化。

评论区
CC BY-NC-SA 4.0 2024 © Plumbiu