From 96835b9a87e01edaa11b838ece69b9212b502d46 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 18 Oct 2022 12:11:12 +0200 Subject: [PATCH 1/3] Add preload for layouts / components --- packages/next/server/app-render.tsx | 43 +++++++++++++------ .../app-rendering/app/ssr-only/slow/layout.js | 8 +++- .../app-rendering/app/ssr-only/slow/page.js | 7 ++- test/e2e/app-dir/rendering.test.ts | 10 ++--- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 236f832e7891f83..7af6b91cf9c5119 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -37,6 +37,19 @@ import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found' import { HeadManagerContext } from '../shared/lib/head-manager-context' import { Writable } from 'stream' +function preloadComponent(Layout: any, props: any) { + try { + let result = Layout(props) + return function () { + // We know what this component will render already. + return result + } + } catch (x) { + // something suspended or errored, try again later + } + return Layout +} + const INTERNAL_HEADERS_INSTANCE = Symbol('internal for headers readonly') function readonlyHeadersError() { @@ -970,7 +983,7 @@ export async function renderToHTMLOrFlight( /** * The React Component to render. */ - const Component = layoutOrPageMod + let Component = layoutOrPageMod ? interopDefault(layoutOrPageMod) : undefined @@ -1091,10 +1104,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()}` : '' @@ -1128,16 +1154,7 @@ export async function renderToHTMLOrFlight( /> )) : null} - + ) }, diff --git a/test/e2e/app-dir/app-rendering/app/ssr-only/slow/layout.js b/test/e2e/app-dir/app-rendering/app/ssr-only/slow/layout.js index 8bdc97f018c96db..a7b1ad6a58e53af 100644 --- a/test/e2e/app-dir/app-rendering/app/ssr-only/slow/layout.js +++ b/test/e2e/app-dir/app-rendering/app/ssr-only/slow/layout.js @@ -1,5 +1,7 @@ import { experimental_use as use } from 'react' +let i + async function getData() { await new Promise((resolve) => setTimeout(resolve, 5000)) return { @@ -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 ( <>

{data.message}

diff --git a/test/e2e/app-dir/app-rendering/app/ssr-only/slow/page.js b/test/e2e/app-dir/app-rendering/app/ssr-only/slow/page.js index c3dd6757a89adbf..7f5cabeb7819dff 100644 --- a/test/e2e/app-dir/app-rendering/app/ssr-only/slow/page.js +++ b/test/e2e/app-dir/app-rendering/app/ssr-only/slow/page.js @@ -1,5 +1,6 @@ import { experimental_use as use } from 'react' +let i async function getData() { await new Promise((resolve) => setTimeout(resolve, 5000)) return { @@ -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 ( <>

{data.message}

diff --git a/test/e2e/app-dir/rendering.test.ts b/test/e2e/app-dir/rendering.test.ts index 494a8494eadd374..c2d5652e1225b00 100644 --- a/test/e2e/app-dir/rendering.test.ts +++ b/test/e2e/app-dir/rendering.test.ts @@ -42,14 +42,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') From e12004b52323330261fd2eda841ddf8e3971670a Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 18 Oct 2022 20:24:05 +0200 Subject: [PATCH 2/3] Hide invalid hook call --- packages/next/server/app-render.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 7af6b91cf9c5119..6ba4fb37aa164f9 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -38,6 +38,15 @@ import { HeadManagerContext } from '../shared/lib/head-manager-context' import { Writable } from 'stream' function preloadComponent(Layout: any, props: any) { + const prev = console.error + console.error = (msg) => { + if (msg.startsWith('Invalid hook call..')) { + // ignore + } else { + // @ts-expect-error argument is defined + prev.apply(console, arguments) + } + } try { let result = Layout(props) return function () { @@ -46,6 +55,8 @@ function preloadComponent(Layout: any, props: any) { } } catch (x) { // something suspended or errored, try again later + } finally { + console.error = prev } return Layout } From 2400d23710415e5ff6162b17a7a790dce07ad4ec Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 1 Nov 2022 14:24:56 +0100 Subject: [PATCH 3/3] Fix merge conflict --- packages/next/server/app-render.tsx | 55 ++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 6e2b8fd2cc10c6f..cda73f9dc01613c 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -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() { @@ -1027,7 +1052,7 @@ export async function renderToHTMLOrFlight( /** * The React Component to render. */ - const Component = layoutOrPageMod + let Component = layoutOrPageMod ? interopDefault(layoutOrPageMod) : undefined @@ -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()}` : '' @@ -1218,16 +1256,7 @@ export async function renderToHTMLOrFlight( /> )) : null} - + {/* {HeadTags ? : null} */} )