DEV Community

Jade Chou
Jade Chou

Posted on

NextJs Codebase Analysis (1) - render callstacks

Terminology

App Router → New routing system using /app (Next.js 13+)

Pages Router → Legacy routing using /pages

SSR → Server-Side Rendering

SSG → Static Site Generation

ISR → Incremental Static Regeneration

CSR → Client-Side Rendering

RSC → React Server Components

SPA → Single Page Application

Beginning of everything: renderImpl

Source code path: packages/next/src/server/render.tsx
https://github.com/vercel/next.js

handleRequest

handleRequestImpl

handleCatchallRenderRequest

render

// response html according to client end's request.
async renderImpl(req, res, pathname, query = {}, parsedUrl, internalRender = false) {
  return this.pipe((ctx) => this.renderToResponse(ctx), {
    req, // request instance
    res, // response instance
    pathname, // request page path from client end, '/page/a', '/page/c', etc
    query // params in request url, '?start=2&side=10'.
  });
}

renderToResponse

renderToResponseImpl

renderToResponseImpl

const result = await this.renderPageComponent(
  {
    ...ctx,
    pathname: match.definition.pathname, // serialize current request path, and get coordinating Component,
    renderOpts: {
      ...ctx.renderOpts,
      params: match.params,
    },
  },
  bubbleNoFallback
)

renderPageComponent

return await this.renderToResponseWithComponents(ctx, result);

renderToResponseWithComponentsImpl

await components.ComponentMod.handler(handlerReq, handlerRes, {
  waitUntil: this.getWaitUntil()
});
Enter fullscreen mode Exit fullscreen mode

get page component findPageComponents

The rendering of a NextJs Page start with a path, Let's reusing the example on NextJs Doc website for demonstration,

it might be "/page", it will trigger renderImpl to analyze its path, and find target component path in renderPageComponent, invoke the function findPageComponentsImpl at packages/next/src/server/next-server.ts

protected async renderPageComponent(
  ctx: RequestContext<ServerRequest, ServerResponse>
) {
  // omitted.........
  const { query, pathname } = ctx
  const appPaths = this.getOriginalAppPaths(pathname)
  let page = pathname
  const result = await this.findPageComponents({ // get coordinate component by current request path.
    locale: getRequestMeta(ctx.req, 'locale'),
    page,
    query,
    params: ctx.renderOpts.params || {},
    // ................
  })
  if (result) {
    return await this.renderToResponseWithComponents(ctx, result) // render component and set result on response instance.
  }
  return false
}
Enter fullscreen mode Exit fullscreen mode

It will keeping invoke function loadComponentsImpl at packages/next/src/server/load-components.ts.

we can see NextJs built server-side bundles under .next, it will be files in dev/server/app that request files under /chunks/ssr(server-side-only) to consist of a intact resource.

using component to sendRenderResult

After found target page component, lest keeping on plumbing logic of renderToResponseWithComponents
renderToResponseWithComponents > renderToResponseWithComponentsImpl

private async renderToResponseWithComponentsImpl()
{
 // omitted.................
 // propagate the request context for dev
 setRequestMeta(request, getRequestMeta(req))
 addRequestMeta(request, 'distDir', this.distDir)
 addRequestMeta..... omitted

 // use previous required component to render result on response instance
 await components.ComponentMod.handler(handlerReq, handlerRes, {
    waitUntil: this.getWaitUntil(),
  })
}
Enter fullscreen mode Exit fullscreen mode

handler is locate at packages\next\src\build\templates\app-page.ts

export async function handler(){
  const prepareResult = await routeModule.prepare(req, res, {
    srcPage,
    multiZoneDraftMode,
  })

  // omitted..............


  await handleResponse(activeSpan)
}
Enter fullscreen mode Exit fullscreen mode

We can just focus on handleResponse

const handleResponse = () => {
  const cacheEntry = await routeModule.handleResponse({
    cacheKey: ssgCacheKey,
    responseGenerator: (c) =>
      responseGenerator({
        span,
        ...c,
      }),
    routeKind: RouteKind.APP_PAGE,
    prerenderManifest,
    // omitted...........
  })

  const { value: cachedData } = cacheEntry

  // This is a request for HTML data.
  const body = cachedData.html

  // If there's no postponed state, we should just serve the HTML. This
// should also be the case for a resume request because it's completed
// as a server render (rather than a static render).
if (!didPostpone || isMinimalMode || isRSCRequest) {
  // normally, for a PPR request, it will come to there to sendRenderResult.
  return sendRenderResult({
    req,
    res,
    generateEtags: nextConfig.generateEtags,
    poweredByHeader: nextConfig.poweredByHeader,
    result: body,
    cacheControl: cacheEntry.cacheControl,
  })
}
}

Enter fullscreen mode Exit fullscreen mode

As we can see, handleResponse will require cached data from routeModule, which would be reused in subsequent sendRenderResult

Summary

We traced the Next.js render pipeline starting from the server request handler down to renderImpl. We followed how Next.js transforms an incoming HTTP request into a rendered result by resolving page components, executing the rendering logic, and finally returning HTML through sendRenderResult.

Next post we will still focus on NextJS codebase, could pay attention if interested, and please feel free to point out any errors or omissions.

Top comments (0)