Skip to content

Commit

Permalink
Render as a concatenation of streams (#28082)
Browse files Browse the repository at this point in the history
Return the `RenderResult` as a concatenation of streams, rather than a concatenation of strings.
  • Loading branch information
devknoll committed Aug 13, 2021
1 parent f95e5fd commit d2551bb
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 45 deletions.
157 changes: 116 additions & 41 deletions packages/next/server/render.tsx
Expand Up @@ -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 =
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1160,54 +1160,129 @@ export async function renderToHTML(
}
}

const bodyRenderIdx = html.indexOf(BODY_RENDER_TARGET)
html =
html.substring(0, bodyRenderIdx) +
(inAmpMode ? '<!-- __NEXT_DATA__ -->' : '') +
docProps.html +
html.substring(bodyRenderIdx + BODY_RENDER_TARGET.length)

let results: Array<RenderResult> = []
const renderTargetIdx = documentHTML.indexOf(BODY_RENDER_TARGET)
results.push(
resultFromChunks([
'<!DOCTYPE html>' + 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(['<!-- __NEXT_DATA__ -->']))
}
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<string>) | 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;amp=1/g, '&amp=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<RenderResult>): Promise<string> {
const result = await resultToChunks(mergeResults(chunks))
return result.join('')
}

if (inAmpMode || hybridAmp) {
// fix &amp being escaped for amphtml rel link
html = html.replace(/&amp;amp=1/g, '&amp=1')
}
function mergeResults(chunks: Array<RenderResult>): 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 {
Expand Down
30 changes: 26 additions & 4 deletions packages/next/server/utils.ts
Expand Up @@ -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
}
}
}

Expand Down

0 comments on commit d2551bb

Please sign in to comment.