From 08a24780b01608e16b7733d31bda2aaf36fd1750 Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Thu, 12 Aug 2021 20:36:54 -0700 Subject: [PATCH] Clean up `Document` in preparation for streaming (#28032) We generate the HTML for a document in two steps: First, we generate the body (i.e. everything under `
`). 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. --- packages/next/pages/_document.tsx | 48 +++----- packages/next/server/render.tsx | 112 ++++++++++--------- packages/next/shared/lib/constants.ts | 2 +- packages/next/shared/lib/document-context.ts | 8 -- packages/next/shared/lib/utils.ts | 19 ++-- 5 files changed, 88 insertions(+), 101 deletions(-) delete mode 100644 packages/next/shared/lib/document-context.ts diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index e11b7386a9ac..7217ce72f94f 100644 --- a/packages/next/pages/_document.tsx +++ b/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' @@ -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 { @@ -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( @@ -91,7 +92,7 @@ function getPreNextScripts(context: DocumentProps, props: OriginProps) { } function getDynamicChunks( - context: DocumentProps, + context: HtmlProps, props: OriginProps, files: DocumentFiles ) { @@ -122,7 +123,7 @@ function getDynamicChunks( } function getScripts( - context: DocumentProps, + context: HtmlProps, props: OriginProps, files: DocumentFiles ) { @@ -176,17 +177,6 @@ export default class Document

extends Component { return { html, head, styles } } - static renderDocument( - DocumentComponent: new () => Document, - props: DocumentProps & Y - ): React.ReactElement { - return ( - - - - ) - } - render() { return ( @@ -206,9 +196,7 @@ export function Html( HTMLHtmlElement > ) { - const { inAmpMode, docComponentsRendered, locale } = useContext( - DocumentComponentContext - ) + const { inAmpMode, docComponentsRendered, locale } = useContext(HtmlContext) docComponentsRendered.Html = true @@ -231,9 +219,9 @@ export class Head extends Component< HTMLHeadElement > > { - static contextType = DocumentComponentContext + static contextType = HtmlContext - context!: React.ContextType + context!: React.ContextType getCssLinks(files: DocumentFiles): JSX.Element[] | null { const { @@ -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

+ if (inAmpMode) return <>{BODY_RENDER_TARGET} + return
{BODY_RENDER_TARGET}
} export class NextScript extends Component { - static contextType = DocumentComponentContext + static contextType = HtmlContext - context!: React.ContextType + context!: React.ContextType // Source: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc static safariNomoduleFix = @@ -773,8 +759,8 @@ export class NextScript extends Component { return getPolyfillScripts(this.context, this.props) } - static getInlineScriptSource(documentProps: Readonly): string { - const { __NEXT_DATA__ } = documentProps + static getInlineScriptSource(context: Readonly): string { + const { __NEXT_DATA__ } = context try { const data = JSON.stringify(__NEXT_DATA__) return htmlEscapeJsonString(data) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index fdc515ed7321..7045073b8c0e 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -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, @@ -39,6 +39,7 @@ import { DocumentInitialProps, DocumentProps, DocumentType, + HtmlContext, getDisplayName, isResSent, loadGetInitialProps, @@ -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 ( '' + ReactDOMServer.renderToStaticMarkup( - {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, - })} + + + ) ) @@ -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) + - `${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 ? '' : '') + + 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) } diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index 8ea2a7d271dd..3b8e639a378e 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -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` diff --git a/packages/next/shared/lib/document-context.ts b/packages/next/shared/lib/document-context.ts deleted file mode 100644 index d3601e7a5b28..000000000000 --- a/packages/next/shared/lib/document-context.ts +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' -import { DocumentProps } from './utils' - -export const DocumentContext = React.createContext(null as any) - -if (process.env.NODE_ENV !== 'production') { - DocumentContext.displayName = 'DocumentContext' -} diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index fdc405dec1cc..8413e120da89 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -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, @@ -26,12 +27,7 @@ export type DocumentType = NextComponentType< DocumentContext, DocumentInitialProps, DocumentProps -> & { - renderDocument( - Document: DocumentType, - props: DocumentProps - ): React.ReactElement -} +> export type AppType = NextComponentType< AppContextType, @@ -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: { @@ -212,6 +210,8 @@ export type DocumentProps = DocumentInitialProps & { scriptLoader: { afterInteractive?: string[]; beforeInteractive?: any[] } locale?: string disableOptimizedLoading?: boolean + styles?: React.ReactElement[] | React.ReactFragment + head?: Array } /** @@ -432,3 +432,8 @@ export const ST = typeof performance.measure === 'function' export class DecodeError extends Error {} + +export const HtmlContext = createContext(null as any) +if (process.env.NODE_ENV !== 'production') { + HtmlContext.displayName = 'HtmlContext' +}