Skip to content

Commit

Permalink
Clean up Document in preparation for streaming (#28032)
Browse files Browse the repository at this point in the history
We generate the HTML for a document in two steps: First, we generate the body (i.e. everything under `<div id="__next">`). Then we generate the rest of the document and embed the body in it.

This doesn't work when the body is a stream, because React can't render the body for us unless we buffer it, and buffering it means not streaming. This PR takes the existing approach for AMP and uses it for all scenarios: instead of rendering HTML, we just render a placeholder that we can replace with HTML later. This will be used in a follow-up PR to let us know where to concatenate the body stream.

I also used the opportunity to split out `HtmlContext` from `DocumentProps`, as these will not be the same thing with functional document components.
  • Loading branch information
devknoll committed Aug 13, 2021
1 parent c8be62c commit 08a2478
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 101 deletions.
48 changes: 17 additions & 31 deletions packages/next/pages/_document.tsx
@@ -1,14 +1,15 @@
import React, { Component, ReactElement, ReactNode, useContext } from 'react'
import flush from 'styled-jsx/server'
import {
AMP_RENDER_TARGET,
BODY_RENDER_TARGET,
OPTIMIZED_FONT_PROVIDERS,
} from '../shared/lib/constants'
import { DocumentContext as DocumentComponentContext } from '../shared/lib/document-context'
import {
DocumentContext,
DocumentInitialProps,
DocumentProps,
HtmlContext,
HtmlProps,
} from '../shared/lib/utils'
import { BuildManifest, getPageFiles } from '../server/get-page-files'
import { cleanAmpPath } from '../server/utils'
Expand Down Expand Up @@ -45,7 +46,7 @@ function getDocumentFiles(
}
}

function getPolyfillScripts(context: DocumentProps, props: OriginProps) {
function getPolyfillScripts(context: HtmlProps, props: OriginProps) {
// polyfills.js has to be rendered as nomodule without async
// It also has to be the first script to load
const {
Expand All @@ -71,7 +72,7 @@ function getPolyfillScripts(context: DocumentProps, props: OriginProps) {
))
}

function getPreNextScripts(context: DocumentProps, props: OriginProps) {
function getPreNextScripts(context: HtmlProps, props: OriginProps) {
const { scriptLoader, disableOptimizedLoading } = context

return (scriptLoader.beforeInteractive || []).map(
Expand All @@ -91,7 +92,7 @@ function getPreNextScripts(context: DocumentProps, props: OriginProps) {
}

function getDynamicChunks(
context: DocumentProps,
context: HtmlProps,
props: OriginProps,
files: DocumentFiles
) {
Expand Down Expand Up @@ -122,7 +123,7 @@ function getDynamicChunks(
}

function getScripts(
context: DocumentProps,
context: HtmlProps,
props: OriginProps,
files: DocumentFiles
) {
Expand Down Expand Up @@ -176,17 +177,6 @@ export default class Document<P = {}> extends Component<DocumentProps & P> {
return { html, head, styles }
}

static renderDocument<Y>(
DocumentComponent: new () => Document<Y>,
props: DocumentProps & Y
): React.ReactElement {
return (
<DocumentComponentContext.Provider value={props}>
<DocumentComponent {...props} />
</DocumentComponentContext.Provider>
)
}

render() {
return (
<Html>
Expand All @@ -206,9 +196,7 @@ export function Html(
HTMLHtmlElement
>
) {
const { inAmpMode, docComponentsRendered, locale } = useContext(
DocumentComponentContext
)
const { inAmpMode, docComponentsRendered, locale } = useContext(HtmlContext)

docComponentsRendered.Html = true

Expand All @@ -231,9 +219,9 @@ export class Head extends Component<
HTMLHeadElement
>
> {
static contextType = DocumentComponentContext
static contextType = HtmlContext

context!: React.ContextType<typeof DocumentComponentContext>
context!: React.ContextType<typeof HtmlContext>

getCssLinks(files: DocumentFiles): JSX.Element[] | null {
const {
Expand Down Expand Up @@ -738,20 +726,18 @@ export class Head extends Component<
}

export function Main() {
const { inAmpMode, html, docComponentsRendered } = useContext(
DocumentComponentContext
)
const { inAmpMode, docComponentsRendered } = useContext(HtmlContext)

docComponentsRendered.Main = true

if (inAmpMode) return <>{AMP_RENDER_TARGET}</>
return <div id="__next" dangerouslySetInnerHTML={{ __html: html }} />
if (inAmpMode) return <>{BODY_RENDER_TARGET}</>
return <div id="__next">{BODY_RENDER_TARGET}</div>
}

export class NextScript extends Component<OriginProps> {
static contextType = DocumentComponentContext
static contextType = HtmlContext

context!: React.ContextType<typeof DocumentComponentContext>
context!: React.ContextType<typeof HtmlContext>

// Source: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
static safariNomoduleFix =
Expand All @@ -773,8 +759,8 @@ export class NextScript extends Component<OriginProps> {
return getPolyfillScripts(this.context, this.props)
}

static getInlineScriptSource(documentProps: Readonly<DocumentProps>): string {
const { __NEXT_DATA__ } = documentProps
static getInlineScriptSource(context: Readonly<HtmlProps>): string {
const { __NEXT_DATA__ } = context
try {
const data = JSON.stringify(__NEXT_DATA__)
return htmlEscapeJsonString(data)
Expand Down
112 changes: 58 additions & 54 deletions packages/next/server/render.tsx
Expand Up @@ -20,7 +20,7 @@ import { GetServerSideProps, GetStaticProps, PreviewData } from '../types'
import { isInAmpMode } from '../shared/lib/amp'
import { AmpStateContext } from '../shared/lib/amp-context'
import {
AMP_RENDER_TARGET,
BODY_RENDER_TARGET,
SERVER_PROPS_ID,
STATIC_PROPS_ID,
STATIC_STATUS_PAGES,
Expand All @@ -39,6 +39,7 @@ import {
DocumentInitialProps,
DocumentProps,
DocumentType,
HtmlContext,
getDisplayName,
isResSent,
loadGetInitialProps,
Expand Down Expand Up @@ -264,54 +265,58 @@ function renderDocument(
autoExport?: boolean
}
): string {
const htmlProps = {
__NEXT_DATA__: {
props, // The result of getInitialProps
page: pathname, // The rendered page
query, // querystring parsed / passed by the user
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
nextExport, // If this is a page exported by `next export`
autoExport, // If this is an auto exported page
isFallback,
dynamicIds:
dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
gsp, // whether the page is getStaticProps
gssp, // whether the page is getServerSideProps
customServer, // whether the user is using a custom server
gip, // whether the page has getInitialProps
appGip, // whether the _app has getInitialProps
locale,
locales,
defaultLocale,
domainLocales,
isPreview,
},
buildManifest,
docComponentsRendered,
dangerousAsPath,
canonicalBase,
ampPath,
inAmpMode,
isDevelopment: !!dev,
hybridAmp,
dynamicImports,
assetPrefix,
headTags,
unstable_runtimeJS,
unstable_JsPreload,
devOnlyCacheBusterQueryString,
scriptLoader,
locale,
disableOptimizedLoading,
styles: docProps.styles,
head: docProps.head,
}
return (
'<!DOCTYPE html>' +
ReactDOMServer.renderToStaticMarkup(
<AmpStateContext.Provider value={ampState}>
{Document.renderDocument(Document, {
__NEXT_DATA__: {
props, // The result of getInitialProps
page: pathname, // The rendered page
query, // querystring parsed / passed by the user
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
nextExport, // If this is a page exported by `next export`
autoExport, // If this is an auto exported page
isFallback,
dynamicIds:
dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
gsp, // whether the page is getStaticProps
gssp, // whether the page is getServerSideProps
customServer, // whether the user is using a custom server
gip, // whether the page has getInitialProps
appGip, // whether the _app has getInitialProps
locale,
locales,
defaultLocale,
domainLocales,
isPreview,
},
buildManifest,
docComponentsRendered,
dangerousAsPath,
canonicalBase,
ampPath,
inAmpMode,
isDevelopment: !!dev,
hybridAmp,
dynamicImports,
assetPrefix,
headTags,
unstable_runtimeJS,
unstable_JsPreload,
devOnlyCacheBusterQueryString,
scriptLoader,
locale,
disableOptimizedLoading,
...docProps,
})}
<HtmlContext.Provider value={htmlProps}>
<Document {...htmlProps} {...docProps} />
</HtmlContext.Provider>
</AmpStateContext.Provider>
)
)
Expand Down Expand Up @@ -1155,16 +1160,15 @@ export async function renderToHTML(
}
}

if (inAmpMode && html) {
// inject HTML to AMP_RENDER_TARGET to allow rendering
// directly to body in AMP mode
const ampRenderIndex = html.indexOf(AMP_RENDER_TARGET)
html =
html.substring(0, ampRenderIndex) +
`<!-- __NEXT_DATA__ -->${docProps.html}` +
html.substring(ampRenderIndex + AMP_RENDER_TARGET.length)
html = await optimizeAmp(html, renderOpts.ampOptimizerConfig)
const bodyRenderIdx = html.indexOf(BODY_RENDER_TARGET)
html =
html.substring(0, bodyRenderIdx) +
(inAmpMode ? '<!-- __NEXT_DATA__ -->' : '') +
docProps.html +
html.substring(bodyRenderIdx + BODY_RENDER_TARGET.length)

if (inAmpMode) {
html = await optimizeAmp(html, renderOpts.ampOptimizerConfig)
if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) {
await renderOpts.ampValidator(html, pathname)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/next/shared/lib/constants.ts
Expand Up @@ -21,7 +21,7 @@ export const BLOCKED_PAGES = ['/_document', '/_app', '/_error']
export const CLIENT_PUBLIC_FILES_PATH = 'public'
export const CLIENT_STATIC_FILES_PATH = 'static'
export const CLIENT_STATIC_FILES_RUNTIME = 'runtime'
export const AMP_RENDER_TARGET = '__NEXT_AMP_RENDER_TARGET__'
export const BODY_RENDER_TARGET = '__NEXT_BODY_RENDER_TARGET__'
export const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__'
// static/runtime/main.js
export const CLIENT_STATIC_FILES_RUNTIME_MAIN = `main`
Expand Down
8 changes: 0 additions & 8 deletions packages/next/shared/lib/document-context.ts

This file was deleted.

19 changes: 12 additions & 7 deletions packages/next/shared/lib/utils.ts
Expand Up @@ -8,6 +8,7 @@ import type { NextRouter } from './router/router'
import type { ParsedUrlQuery } from 'querystring'
import type { PreviewData } from 'next/types'
import type { UrlObject } from 'url'
import { createContext } from 'react'

export type NextComponentType<
C extends BaseContext = NextPageContext,
Expand All @@ -26,12 +27,7 @@ export type DocumentType = NextComponentType<
DocumentContext,
DocumentInitialProps,
DocumentProps
> & {
renderDocument(
Document: DocumentType,
props: DocumentProps
): React.ReactElement
}
>

export type AppType = NextComponentType<
AppContextType,
Expand Down Expand Up @@ -188,7 +184,9 @@ export type DocumentInitialProps = RenderPageResult & {
styles?: React.ReactElement[] | React.ReactFragment
}

export type DocumentProps = DocumentInitialProps & {
export type DocumentProps = DocumentInitialProps & HtmlProps

export type HtmlProps = {
__NEXT_DATA__: NEXT_DATA
dangerousAsPath: string
docComponentsRendered: {
Expand All @@ -212,6 +210,8 @@ export type DocumentProps = DocumentInitialProps & {
scriptLoader: { afterInteractive?: string[]; beforeInteractive?: any[] }
locale?: string
disableOptimizedLoading?: boolean
styles?: React.ReactElement[] | React.ReactFragment
head?: Array<JSX.Element | null>
}

/**
Expand Down Expand Up @@ -432,3 +432,8 @@ export const ST =
typeof performance.measure === 'function'

export class DecodeError extends Error {}

export const HtmlContext = createContext<HtmlProps>(null as any)
if (process.env.NODE_ENV !== 'production') {
HtmlContext.displayName = 'HtmlContext'
}

0 comments on commit 08a2478

Please sign in to comment.