From d2551bbbc7588d392fd47420c4eea803794f967f Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 09:40:12 -0700 Subject: [PATCH] Render as a concatenation of streams (#28082) Return the `RenderResult` as a concatenation of streams, rather than a concatenation of strings. --- packages/next/server/render.tsx | 157 +++++++++++++++++++++++--------- packages/next/server/utils.ts | 30 +++++- 2 files changed, 142 insertions(+), 45 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 7045073b8c0e43b..1f98e28c862b492 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -63,7 +63,7 @@ import { Redirect, } from '../lib/load-custom-routes' import { DomainLocale } from './config' -import { RenderResult, resultFromChunks } from './utils' +import { RenderResult, resultFromChunks, resultToChunks } from './utils' function noRouter() { const message = @@ -1099,7 +1099,7 @@ export async function renderToHTML( const docComponentsRendered: DocumentProps['docComponentsRendered'] = {} - let html = renderDocument(Document, { + const documentHTML = renderDocument(Document, { ...renderOpts, canonicalBase: !renderOpts.ampPath && (req as any).__nextStrippedLocale @@ -1160,54 +1160,129 @@ export async function renderToHTML( } } - const bodyRenderIdx = html.indexOf(BODY_RENDER_TARGET) - html = - html.substring(0, bodyRenderIdx) + - (inAmpMode ? '' : '') + - docProps.html + - html.substring(bodyRenderIdx + BODY_RENDER_TARGET.length) - + let results: Array = [] + const renderTargetIdx = documentHTML.indexOf(BODY_RENDER_TARGET) + results.push( + resultFromChunks([ + '' + documentHTML.substring(0, renderTargetIdx), + ]) + ) if (inAmpMode) { - html = await optimizeAmp(html, renderOpts.ampOptimizerConfig) - if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) { - await renderOpts.ampValidator(html, pathname) - } + results.push(resultFromChunks([''])) } + results.push(resultFromChunks([docProps.html])) + results.push( + resultFromChunks([ + documentHTML.substring(renderTargetIdx + BODY_RENDER_TARGET.length), + ]) + ) - // Avoid postProcess if both flags are false - if (process.env.__NEXT_OPTIMIZE_FONTS || process.env.__NEXT_OPTIMIZE_IMAGES) { - html = await postProcess( - html, - { getFontDefinition }, - { - optimizeFonts: renderOpts.optimizeFonts, - optimizeImages: renderOpts.optimizeImages, + const postProcessors: Array<((html: string) => Promise) | null> = [ + inAmpMode + ? async (html: string) => { + html = await optimizeAmp(html, renderOpts.ampOptimizerConfig) + if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) { + await renderOpts.ampValidator(html, pathname) + } + return html + } + : null, + process.env.__NEXT_OPTIMIZE_FONTS || process.env.__NEXT_OPTIMIZE_IMAGES + ? async (html: string) => { + return await postProcess( + html, + { getFontDefinition }, + { + optimizeFonts: renderOpts.optimizeFonts, + optimizeImages: renderOpts.optimizeImages, + } + ) + } + : null, + renderOpts.optimizeCss + ? async (html: string) => { + // eslint-disable-next-line import/no-extraneous-dependencies + const Critters = require('critters') + const cssOptimizer = new Critters({ + ssrMode: true, + reduceInlineStyles: false, + path: renderOpts.distDir, + publicPath: `${renderOpts.assetPrefix}/_next/`, + preload: 'media', + fonts: false, + ...renderOpts.optimizeCss, + }) + return await cssOptimizer.process(html) + } + : null, + inAmpMode || hybridAmp + ? async (html: string) => { + return html.replace(/&amp=1/g, '&=1') + } + : null, + ].filter(Boolean) + + if (postProcessors.length > 0) { + let html = await resultsToString(results) + for (const postProcessor of postProcessors) { + if (postProcessor) { + html = await postProcessor(html) } - ) + } + results = [resultFromChunks([html])] } - if (renderOpts.optimizeCss) { - // eslint-disable-next-line import/no-extraneous-dependencies - const Critters = require('critters') - const cssOptimizer = new Critters({ - ssrMode: true, - reduceInlineStyles: false, - path: renderOpts.distDir, - publicPath: `${renderOpts.assetPrefix}/_next/`, - preload: 'media', - fonts: false, - ...renderOpts.optimizeCss, - }) + return mergeResults(results) +} - html = await cssOptimizer.process(html) - } +async function resultsToString(chunks: Array): Promise { + const result = await resultToChunks(mergeResults(chunks)) + return result.join('') +} - if (inAmpMode || hybridAmp) { - // fix & being escaped for amphtml rel link - html = html.replace(/&amp=1/g, '&=1') - } +function mergeResults(chunks: Array): RenderResult { + return ({ next, complete, error }) => { + let idx = 0 + let canceled = false + let unsubscribe = () => {} + + const subscribeNext = () => { + if (canceled) { + return + } - return resultFromChunks([html]) + if (idx < chunks.length) { + const result = chunks[idx++] + unsubscribe = result({ + next, + complete() { + unsubscribe() + subscribeNext() + }, + error(err) { + unsubscribe() + if (!canceled) { + canceled = true + error(err) + } + }, + }) + } else { + if (!canceled) { + canceled = true + complete() + } + } + } + subscribeNext() + + return () => { + if (!canceled) { + canceled = true + unsubscribe() + } + } + } } function errorToJSON(err: Error): Error { diff --git a/packages/next/server/utils.ts b/packages/next/server/utils.ts index 22778be886a9237..e898038ecb47d97 100644 --- a/packages/next/server/utils.ts +++ b/packages/next/server/utils.ts @@ -24,10 +24,32 @@ export type RenderResult = (observer: { }) => Disposable export function resultFromChunks(chunks: string[]): RenderResult { - return ({ next, complete }) => { - chunks.forEach(next) - complete() - return () => {} + return ({ next, complete, error }) => { + let canceled = false + process.nextTick(() => { + try { + for (const chunk of chunks) { + if (canceled) { + return + } + next(chunk) + } + } catch (err) { + if (!canceled) { + canceled = true + error(err) + } + } + + if (!canceled) { + canceled = true + complete() + } + }) + + return () => { + canceled = true + } } }