Skip to content

Commit

Permalink
Add preload for layouts / components (#41519)
Browse files Browse the repository at this point in the history
Preload layout/component so that fetching starts eagerly.
<!--
Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change that you're making:
-->

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
timneutkens committed Nov 1, 2022
1 parent b72dc5b commit 1af21b5
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 20 deletions.
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..')) {
// 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
8 changes: 7 additions & 1 deletion test/e2e/app-dir/app-rendering/app/ssr-only/slow/layout.js
@@ -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

0 comments on commit 1af21b5

Please sign in to comment.