Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render as a concatenation of streams #28082

Merged
merged 3 commits into from Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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