From 3a67a0789af66dea34f5e2247b4d2ecaad5a2b93 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 30 Apr 2024 22:40:11 +0200 Subject: [PATCH] Remove extra suspense boundary for default next/dynamic (#64716) ### What Removing the Suspense boundary on top of `next/dynamic` by default, make it as `React.lazy` component with preloading CSS feature. ### Why Extra Suspense boundary is causing extra useless rendering. For SSR, it shouldn't render `loading` by default Related: #64060 Related: #64687 Closes [NEXT-3074](https://linear.app/vercel/issue/NEXT-3074/app-router-content-flickering-with-reactcreatecontext-and-nextdynamic) This is sort of a breaking change, since removing the Suspense boundary on top of `next/dynamic` by default. If there's error happening in side the dynamic component you need to wrap an extra Suspense boundary on top of it --- packages/next/src/shared/lib/dynamic.tsx | 1 + .../src/shared/lib/lazy-dynamic/loadable.tsx | 9 +++++--- .../dynamic/app/default/dynamic-component.js | 12 ++++++++++ test/e2e/app-dir/dynamic/app/default/page.js | 17 ++++++++++++++ test/e2e/app-dir/dynamic/dynamic.test.ts | 23 ++++++++++++++++++- 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 test/e2e/app-dir/dynamic/app/default/dynamic-component.js create mode 100644 test/e2e/app-dir/dynamic/app/default/page.js diff --git a/packages/next/src/shared/lib/dynamic.tsx b/packages/next/src/shared/lib/dynamic.tsx index 52f68be1f73ec..64b10dd837e49 100644 --- a/packages/next/src/shared/lib/dynamic.tsx +++ b/packages/next/src/shared/lib/dynamic.tsx @@ -41,6 +41,7 @@ export type DynamicOptions

= LoadableGeneratedOptions & { loadableGenerated?: LoadableGeneratedOptions ssr?: boolean /** + * TODO: remove this in next major version v15 * @deprecated `suspense` prop is not required anymore */ suspense?: boolean diff --git a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx index 31b6e459e93d8..0c1ac814783fe 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx +++ b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx @@ -1,4 +1,4 @@ -import { Suspense, lazy } from 'react' +import { Suspense, Fragment, lazy } from 'react' import { BailoutToCSR } from './dynamic-bailout-to-csr' import type { ComponentModule } from './types' import { PreloadChunks } from './preload-chunks' @@ -48,7 +48,10 @@ function Loadable(options: LoadableOptions) { ) : null - const children = opts.ssr ? ( + const isSSR = opts.ssr + const Wrap = isSSR ? Fragment : Suspense + const wrapProps = isSSR ? {} : { fallback: fallbackElement } + const children = isSSR ? ( <> {/* During SSR, we need to preload the CSS from the dynamic component to avoid flash of unstyled content */} {typeof window === 'undefined' ? ( @@ -62,7 +65,7 @@ function Loadable(options: LoadableOptions) { ) - return {children} + return {children} } LoadableComponent.displayName = 'LoadableComponent' diff --git a/test/e2e/app-dir/dynamic/app/default/dynamic-component.js b/test/e2e/app-dir/dynamic/app/default/dynamic-component.js new file mode 100644 index 0000000000000..27d3b267db32d --- /dev/null +++ b/test/e2e/app-dir/dynamic/app/default/dynamic-component.js @@ -0,0 +1,12 @@ +const isDevTest = false + +const DynamicImportComponent = () => { + if (isDevTest && typeof window === 'undefined') { + throw new Error('This component should only be rendered on the client side') + } + return ( +

This is a dynamically imported component
+ ) +} + +export default DynamicImportComponent diff --git a/test/e2e/app-dir/dynamic/app/default/page.js b/test/e2e/app-dir/dynamic/app/default/page.js new file mode 100644 index 0000000000000..b40d356d55955 --- /dev/null +++ b/test/e2e/app-dir/dynamic/app/default/page.js @@ -0,0 +1,17 @@ +'use client' + +import dynamic from 'next/dynamic' + +const DynamicHeader = dynamic(() => import('./dynamic-component'), { + loading: () =>

Loading...

, +}) + +const ClientWrapper = () => { + return ( +
+ +
+ ) +} + +export default ClientWrapper diff --git a/test/e2e/app-dir/dynamic/dynamic.test.ts b/test/e2e/app-dir/dynamic/dynamic.test.ts index 9322396bf405c..590ae9509e054 100644 --- a/test/e2e/app-dir/dynamic/dynamic.test.ts +++ b/test/e2e/app-dir/dynamic/dynamic.test.ts @@ -1,7 +1,8 @@ import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' describe('app dir - next/dynamic', () => { - const { next, isNextStart, skipped } = nextTestSetup({ + const { next, isNextStart, isNextDev, skipped } = nextTestSetup({ files: __dirname, skipDeployment: true, }) @@ -57,6 +58,26 @@ describe('app dir - next/dynamic', () => { expect($('h1').text()).toBe('hello') }) + it('should not render loading by default', async () => { + const $ = await next.render$('/default') + expect($('#dynamic-component').text()).not.toContain('loading') + }) + + if (isNextDev) { + it('should directly raise error when dynamic component error on server', async () => { + const pagePath = 'app/default/dynamic-component.js' + const page = await next.readFile(pagePath) + await next.patchFile( + pagePath, + page.replace('const isDevTest = false', 'const isDevTest = true') + ) + await retry(async () => { + const { status } = await next.fetch('/default') + expect(status).toBe(500) + }) + }) + } + describe('no SSR', () => { it('should not render client component imported through ssr: false in client components in edge runtime', async () => { // noSSR should not show up in html