From d0c71166d09e2fcdf84fdd185d165cfd12d95b84 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 21 Apr 2022 16:03:25 +0200 Subject: [PATCH 1/2] ensure there is only 1 render pass when using _document gIP --- packages/next/server/render.tsx | 92 ++++++++++++++----- .../basic/styled-components.test.ts | 11 +++ .../basic/styled-components/pages/index.js | 1 + test/integration/react-18/app/pages/index.js | 1 + test/integration/react-18/test/basics.js | 5 + test/lib/next-test-utils.js | 6 ++ 6 files changed, 93 insertions(+), 23 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 824dc200b1ee..2f87918b0526 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -395,7 +395,6 @@ const useFlightResponse = createFlightHook() // Create the wrapper component for a Flight stream. function createServerComponentRenderer( - AppMod: any, ComponentMod: any, { cachePrefix, @@ -415,10 +414,13 @@ function createServerComponentRenderer( globalThis.__webpack_require__ = ComponentMod.__next_rsc__.__webpack_require__ const Component = interopDefault(ComponentMod) - function ServerComponentWrapper(props: any) { + function ServerComponentWrapper({ App, router, ...props }: any) { const id = (React as any).useId() + const reqStream: ReadableStream = renderToReadableStream( - renderFlight(AppMod, ComponentMod, props), + + + , serverComponentManifest ) @@ -520,7 +522,7 @@ export async function renderToHTML( if (isServerComponent) { serverComponentsInlinedTransformStream = new TransformStream() const search = urlQueryToSearchParams(query).toString() - Component = createServerComponentRenderer(AppMod, ComponentMod, { + Component = createServerComponentRenderer(ComponentMod, { cachePrefix: pathname + (search ? `?${search}` : ''), inlinedTransformStream: serverComponentsInlinedTransformStream, staticTransformStream: serverComponentsPageDataTransformStream, @@ -1311,7 +1313,15 @@ export async function renderToHTML( } } - async function documentInitialProps() { + async function documentInitialProps( + renderShell?: ({ + EnhancedApp, + EnhancedComponent, + }: { + EnhancedApp?: AppType + EnhancedComponent?: NextComponentType + }) => Promise + ) { const renderPage: RenderPage = ( options: ComponentsEnhancer = {} ): RenderPageResult | Promise => { @@ -1333,6 +1343,14 @@ export async function renderToHTML( const { App: EnhancedApp, Component: EnhancedComponent } = enhanceComponents(options, App, Component) + if (renderShell) { + return renderShell({ EnhancedApp, EnhancedComponent }).then(() => { + // When using concurrent features, we don't have or need the full + // html so it's fine to return nothing here. + return { html: '', head } + }) + } + const html = ReactDOMServer.renderToString( @@ -1364,7 +1382,13 @@ export async function renderToHTML( return { docProps, documentCtx } } - const renderContent = () => { + const renderContent = ({ + EnhancedApp, + EnhancedComponent, + }: { + EnhancedApp?: AppType + EnhancedComponent?: NextComponentType + } = {}) => { return ctx.err && ErrorDebug ? ( @@ -1372,12 +1396,16 @@ export async function renderToHTML( ) : ( - {isServerComponent && !!AppMod.__next_rsc__ ? ( - // _app.server.js is used. - - ) : ( - - )} + {isServerComponent + ? React.createElement(EnhancedComponent || Component, { + App: EnhancedApp || App, + ...props.pageProps, + }) + : React.createElement(EnhancedApp || App, { + ...props, + Component: EnhancedComponent || Component, + router, + })} ) @@ -1419,13 +1447,23 @@ export async function renderToHTML( } } } else { - // We start rendering the shell earlier, before returning the head tags - // to `documentResult`. - const content = renderContent() - const renderStream = await renderToInitialStream({ - ReactDOMServer, - element: content, - }) + let renderStream: ReadableStream & { + allReady?: Promise | undefined + } + + const renderShell = async ({ + EnhancedApp, + EnhancedComponent, + }: { + EnhancedApp?: AppType + EnhancedComponent?: NextComponentType + } = {}) => { + const content = renderContent({ EnhancedApp, EnhancedComponent }) + renderStream = await renderToInitialStream({ + ReactDOMServer, + element: content, + }) + } const bodyResult = async (suffix: string) => { // this must be called inside bodyResult so appWrappers is @@ -1494,10 +1532,18 @@ export async function renderToHTML( !Document.getInitialProps ) - const documentInitialPropsRes = hasDocumentGetInitialProps - ? await documentInitialProps() - : {} - if (documentInitialPropsRes === null) return null + // If it has getInitialProps, we will render the shell in `renderPage`. + // Otherwise we do it right now. + let documentInitialPropsRes: + | {} + | Awaited> + if (hasDocumentGetInitialProps) { + documentInitialPropsRes = await documentInitialProps(renderShell) + if (documentInitialPropsRes === null) return null + } else { + await renderShell() + documentInitialPropsRes = {} + } const { docProps } = (documentInitialPropsRes as any) || {} const documentElement = () => { diff --git a/test/development/basic/styled-components.test.ts b/test/development/basic/styled-components.test.ts index a7c9892c98f6..458dd9508694 100644 --- a/test/development/basic/styled-components.test.ts +++ b/test/development/basic/styled-components.test.ts @@ -73,4 +73,15 @@ describe('styled-components SWC transform', () => { expect(html).toContain('background:transparent') expect(html).toContain('color:white') }) + + it('should only render once on the server per request', async () => { + const outputs = [] + next.on('stdout', (args) => { + outputs.push(args) + }) + await renderViaHTTP(next.url, '/') + expect( + outputs.filter((output) => output.trim() === '__render__').length + ).toBe(1) + }) }) diff --git a/test/development/basic/styled-components/pages/index.js b/test/development/basic/styled-components/pages/index.js index 31c5716fbf10..8c833028979d 100644 --- a/test/development/basic/styled-components/pages/index.js +++ b/test/development/basic/styled-components/pages/index.js @@ -21,6 +21,7 @@ const Button = styled.a` ` export default function Home() { + console.log('__render__') return (