Next.js 未能实时渲染问题排查

image

最近更文较少, 主要忙于各大团购群买菜 + 做饭 + 做核酸 + 远程办公。从 3月30号 到今天小区已经封了 11 天, 上海疫情又创了新高 1015 + 22609 例。只希望这波疫情赶紧结束, 不要再出负面新闻了 😓 。图片来自封控前的一次囤货外出。

背景

近期在对 SSR 项目进行 CDN和域名灾备 的改造, 同学 a 负责的 Next.js 项目改造测试时发现 CDN 没有预期内的动态切换。这个项目一直有历史包袱, 我想着不会线上 Node 一直是挂的吧, 难道长久以来都是 Nginx 返回的静态兜底页面?

问题排查

本地启动了该项目, 发现无论是服务端渲染请求还是健康检查 Node 服务都没有异常。得出 Node 大概率是一直正常运行的, 那么会是其他什么原因导致的一直返回的是静态资源而非实时的 SSR 直出了?

真正的原因是当你的 src/pages/_app.tsx 文件中导出的 App 组件或者某个 Page 组件没有定义 getInitialProps 或者 getServerSideProps 这个静态方法, 意味着你其实是不需要在服务端进行注水(比如拉取用户的某个真实数据接口, 然后实时渲染直出)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// demo

function Page({ stars }) {
return <div>Next stars: {stars}</div>
}

Page.getInitialProps = async (ctx) => {
const res = await fetch('https://api.github.com/repos/vercel/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}

export default Page

此时 Next.js 会在打包构建阶段预渲染一个静态 html, 到项目发布成功 Node 服务开始运行的时候直接返回该静态 html 即可。因为不需要注水, html 的最终形态构建时就能决定下来了, 没必要服务端再实时渲染, 既能减少 CPU 消耗又能难缩短 rt。

后面查阅了一下文档, Next.js 称这个优化点为 Automatic Static Optimization

代码实现

构建时

可以看到 isStatic 的值为 true 的条件, 即导出的 App 组件没有定义 getStaticProps、 getInitialProps、getServerSideProps 任意一个静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// packages/next/build/utils.ts

export async function isPageStatic(
// ...
}> {
return isPageStaticSpan.traceAsyncFn(async () => {
try {

const mod = await loadComponents(distDir, page, serverless)
const Comp = mod.Component

const hasFlightData = !!(mod as any).__next_rsc__
const hasGetInitialProps = !!(Comp as any).getInitialProps
const hasStaticProps = !!mod.getStaticProps
const hasStaticPaths = !!mod.getStaticPaths
const hasServerProps = !!mod.getServerSideProps

// ...

return {
isStatic:
!hasStaticProps &&
!hasGetInitialProps &&
!hasServerProps &&
!hasFlightData,
isHybridAmp: config.amp === 'hybrid',
// ...
}
} catch (err) {
if (isError(err) && err.code === 'MODULE_NOT_FOUND') return {}
throw err
}
})
}

对于 isStatic 的值为 true 的页面 (staticPages) 和定义了 getStaticProps(ssgPages) 静态方法的页面就会通过 exportApp 方法进行预渲染生成静态 html。

  • exportApp 的预渲染是调用的 ReactDOMServer.renderToString, 并非是借助的 puppeteer
  • next export 命令也是调用的 exportApp 方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // packages/next/build/index.ts

    export default async function build(
    dir: string,
    conf = null,
    reactProductionProfiling = false,
    debugOutput = false,
    runLint = true
    ): Promise<void> {
    // ...

    const combinedPages = [...staticPages, ...ssgPages]

    if (combinedPages.length > 0 || useStatic404 || useDefaultStatic500) {
    // ...
    const exportApp: typeof import('../export').default =
    require('../export').default
    const exportOptions = {
    // ...
    }
    const exportConfig: any = {
    // ...
    }

    await exportApp(dir, exportOptions, nextBuildSpan, exportConfig)

    // ...
    }
    构建阶段生成好静态的 html 文件, 服务运行时直接返回即可

image

对于定义了 getInitialProps 或者 getServerSideProps 静态方法的组件构建阶段则只会生成服务端运行时需要的 js 文件

image

运行时

如果发现所需组件是一个 html 文件, requirePage 方法就会返回该 html 的字符串。如果是一个 js 文件则正常返回该模块的 module.exports, 然后服务端渲染该组件生成 html 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// packages/next/server/load-components.ts

export async function loadComponents(
distDir: string,
pathname: string,
serverless: boolean,
serverComponents?: boolean
): Promise<LoadComponentsReturnType> {
if (serverless) {
const ComponentMod = await requirePage(pathname, distDir, serverless)
if (typeof ComponentMod === 'string') {
return {
Component: ComponentMod as any,
pageConfig: {},
ComponentMod,
} as LoadComponentsReturnType
}

let {
default: Component,
getStaticProps,
getStaticPaths,
getServerSideProps,
} = ComponentMod

Component = await Component
getStaticProps = await getStaticProps
getStaticPaths = await getStaticPaths
getServerSideProps = await getServerSideProps
const pageConfig = (await ComponentMod.config) || {}

return {
Component,
pageConfig,
getStaticProps,
getStaticPaths,
getServerSideProps,
ComponentMod,
} as LoadComponentsReturnType
}

// ...
}

代码看到这里我们知道了

  • getInitialProps 和 getServerSideProps 是服务端渲染时才会运行的用于注水的钩子函数, Next.js 推荐使用 getServerSideProps 函数
  • getStaticProps 是构建时才会运行的用于注水的钩子函数

如何区分

通过下面的代码可以看见可以通过 Next.js 注入的 NEXT_DATA 的 gsp, gssp, gip 等值即可以快速判断当前页面是何种方式进行的渲染方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// packages/next/server/render.tsx

export async function renderToHTML(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: NextParsedUrlQuery,
renderOpts: RenderOpts
): Promise<RenderResult | null> {
// ...

const htmlProps: HtmlProps = {
__NEXT_DATA__: {
props, // The result of getInitialProps
nextExport: nextExport === true ? true : undefined, // If this is a page exported by `next export`
autoExport: isAutoExport === true ? true : undefined, // If this is an auto exported page
gsp: !!getStaticProps ? true : undefined, // whether the page is getStaticProps
gssp: !!getServerSideProps ? true : undefined, // whether the page is getServerSideProps
rsc: isServerComponent ? true : undefined, // whether the page is a server components page
customServer, // whether the user is using a custom server
gip: hasPageGetInitialProps ? true : undefined, // whether the page has getInitialProps
appGip: !defaultAppGetInitialProps ? true : undefined, // whether the _app has getInitialProps
// ...
},
}

// ...

return new RenderResult(chainStreams(streams))
}

知道如何区分后回头看看该项目返回的 html 就可以快速知道该页面是没有定义 getStaticProps、getInitialProps、getServerSideProps 任意一个静态方法的即 isStatic 为 true 的静态页面
image

小结

由于没有定义 getInitialProps、getServerSideProps 任意一个静态方法故 Node 实时的获取 CDN 配置信息以及渲染的逻辑没有运行, 该页面的请求都是 Node 直接返回的静态 html。