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

Add preload for layouts / components #41519

Merged
merged 5 commits into from Nov 1, 2022
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
55 changes: 42 additions & 13 deletions packages/next/server/app-render.tsx
Expand Up @@ -39,6 +39,31 @@ import { HeadManagerContext } from '../shared/lib/head-manager-context'
import { Writable } from 'stream'
import stringHash from 'next/dist/compiled/string-hash'

function preloadComponent(Component: any, props: any) {
const prev = console.error
// Hide invalid hook call warning when calling component
console.error = (msg) => {
if (msg.startsWith('Invalid hook call..')) {
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
// ignore
} else {
// @ts-expect-error argument is defined
prev.apply(console, arguments)
}
}
try {
let result = Component(props)
return function () {
// We know what this component will render already.
return result
}
} catch (x) {
// something suspended or errored, try again later
} finally {
console.error = prev
}
return Component
}

const INTERNAL_HEADERS_INSTANCE = Symbol('internal for headers readonly')

function readonlyHeadersError() {
Expand Down Expand Up @@ -1027,7 +1052,7 @@ export async function renderToHTMLOrFlight(
/**
* The React Component to render.
*/
const Component = layoutOrPageMod
let Component = layoutOrPageMod
? interopDefault(layoutOrPageMod)
: undefined

Expand Down Expand Up @@ -1181,10 +1206,23 @@ export async function renderToHTMLOrFlight(
}
}

const props = {
...parallelRouteComponents,
// TODO-APP: params and query have to be blocked parallel route names. Might have to add a reserved name list.
// Params are always the current params that apply to the layout
// If you have a `/dashboard/[team]/layout.js` it will provide `team` as a param but not anything further down.
params: currentParams,
// Query is only provided to page
...(isPage ? { searchParams: query } : {}),
}

// Eagerly execute layout/page component to trigger fetches early.
Component = await Promise.resolve().then(() => {
return preloadComponent(Component, props)
})

return {
Component: () => {
let props = {}

// Add extra cache busting (DEV only) for https://github.com/vercel/next.js/issues/5860
// See also https://bugs.webkit.org/show_bug.cgi?id=187726
const cacheBustingUrlSuffix = dev ? `?ts=${Date.now()}` : ''
Expand Down Expand Up @@ -1218,16 +1256,7 @@ export async function renderToHTMLOrFlight(
/>
))
: null}
<Component
{...props}
{...parallelRouteComponents}
// TODO-APP: params and query have to be blocked parallel route names. Might have to add a reserved name list.
// Params are always the current params that apply to the layout
// If you have a `/dashboard/[team]/layout.js` it will provide `team` as a param but not anything further down.
params={currentParams}
// Query is only provided to page
{...(isPage ? { searchParams: query } : {})}
/>
<Component {...props} />
{/* {HeadTags ? <HeadTags /> : null} */}
</>
)
Expand Down
@@ -1,5 +1,7 @@
import { use } from 'react'

let i

async function getData() {
await new Promise((resolve) => setTimeout(resolve, 5000))
return {
Expand All @@ -8,7 +10,11 @@ async function getData() {
}

export default function gsspLayout(props) {
const data = use(getData())
// TODO-APP: refactor this test page to `async function` instead.
if (!i) {
i = getData()
}
const data = use(i)
return (
<>
<h1 id="slow-layout-message">{data.message}</h1>
Expand Down
7 changes: 6 additions & 1 deletion test/e2e/app-dir/app-rendering/app/ssr-only/slow/page.js
@@ -1,5 +1,6 @@
import { use } from 'react'

let i
async function getData() {
await new Promise((resolve) => setTimeout(resolve, 5000))
return {
Expand All @@ -8,7 +9,11 @@ async function getData() {
}

export default function nestedPage(props) {
const data = use(getData())
// TODO-APP: refactor this test page to `async function` instead.
if (!i) {
i = getData()
}
const data = use(i)
return (
<>
<p id="slow-page-message">{data.message}</p>
Expand Down
10 changes: 5 additions & 5 deletions test/e2e/app-dir/rendering.test.ts
Expand Up @@ -37,14 +37,14 @@ describe('app dir rendering', () => {
expect($('#page-message').text()).toBe('hello from page')
})

it('should run data in parallel', async () => {
// const startTime = Date.now()
it('should run data fetch in parallel', async () => {
const startTime = Date.now()
const html = await renderViaHTTP(next.url, '/ssr-only/slow')
// const endTime = Date.now()
// const duration = endTime - startTime
const endTime = Date.now()
const duration = endTime - startTime
// Each part takes 5 seconds so it should be below 10 seconds
// Using 7 seconds to ensure external factors causing slight slowness don't fail the tests
// expect(duration < 7000).toBe(true)
expect(duration < 7000).toBe(true)
const $ = cheerio.load(html)
expect($('#slow-layout-message').text()).toBe('hello from slow layout')
expect($('#slow-page-message').text()).toBe('hello from slow page')
Expand Down