diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 31ed619c8b27a..43a13398cf28a 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -406,9 +406,17 @@ export function getDefineEnv({ } } +function getReactProfilingInProduction() { + return { + 'react-dom$': 'react-dom/profiling', + 'scheduler/tracing': 'scheduler/tracing-profiling', + } +} + function createRSCAliases( bundledReactChannel: string, opts: { + reactProductionProfiling: boolean reactSharedSubset: boolean reactDomServerRenderingStub: boolean reactServerCondition?: boolean @@ -455,6 +463,15 @@ function createRSCAliases( } } + if (opts.reactProductionProfiling) { + alias[ + 'react-dom$' + ] = `next/dist/compiled/react-dom${bundledReactChannel}/profiling` + alias[ + 'scheduler/tracing' + ] = `next/dist/compiled/scheduler${bundledReactChannel}/tracing-profiling` + } + return alias } @@ -998,15 +1015,6 @@ export default async function getBaseWebpackConfig( } as ClientEntries) : undefined - function getReactProfilingInProduction() { - if (reactProductionProfiling) { - return { - 'react-dom$': 'react-dom/profiling', - 'scheduler/tracing': 'scheduler/tracing-profiling', - } - } - } - // tell webpack where to look for _app and _document // using aliases to allow falling back to the default // version when removed or not present @@ -1145,7 +1153,7 @@ export default async function getBaseWebpackConfig( [ROOT_DIR_ALIAS]: dir, [DOT_NEXT_ALIAS]: distDir, ...(isClient || isEdgeServer ? getOptimizedAliases() : {}), - ...getReactProfilingInProduction(), + ...(reactProductionProfiling ? getReactProfilingInProduction() : {}), [RSC_ACTION_VALIDATE_ALIAS]: 'next/dist/build/webpack/loaders/next-flight-loader/action-validate', @@ -1933,6 +1941,7 @@ export default async function getBaseWebpackConfig( ...createRSCAliases(bundledReactChannel, { reactSharedSubset: false, reactDomServerRenderingStub: false, + reactProductionProfiling, }), }, }, @@ -1965,6 +1974,8 @@ export default async function getBaseWebpackConfig( reactSharedSubset: true, reactDomServerRenderingStub: true, reactServerCondition: true, + // No server components profiling + reactProductionProfiling, }), }, }, @@ -2028,6 +2039,7 @@ export default async function getBaseWebpackConfig( reactSharedSubset: true, reactDomServerRenderingStub: true, reactServerCondition: true, + reactProductionProfiling, }), }, }, @@ -2041,6 +2053,7 @@ export default async function getBaseWebpackConfig( reactSharedSubset: false, reactDomServerRenderingStub: true, reactServerCondition: false, + reactProductionProfiling, }), }, }, @@ -2057,6 +2070,7 @@ export default async function getBaseWebpackConfig( reactSharedSubset: false, reactDomServerRenderingStub: false, reactServerCondition: false, + reactProductionProfiling, }), }, }, diff --git a/packages/next/src/compiled/react-dom-experimental/profiling.js b/packages/next/src/compiled/react-dom-experimental/profiling.js new file mode 100644 index 0000000000000..91f89f07ffe13 --- /dev/null +++ b/packages/next/src/compiled/react-dom-experimental/profiling.js @@ -0,0 +1,38 @@ +'use strict'; + +function checkDCE() { + /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ + if ( + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined' || + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE !== 'function' + ) { + return; + } + if (process.env.NODE_ENV !== 'production') { + // This branch is unreachable because this function is only called + // in production, but the condition is true only in development. + // Therefore if the branch is still here, dead code elimination wasn't + // properly applied. + // Don't change the message. React DevTools relies on it. Also make sure + // this message doesn't occur elsewhere in this function, or it will cause + // a false positive. + throw new Error('^_^'); + } + try { + // Verify that the code above has been dead code eliminated (DCE'd). + __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); + } catch (err) { + // DevTools shouldn't crash React, no matter what. + // We should still report in case we break this code. + console.error(err); + } +} + +if (process.env.NODE_ENV === 'production') { + // DCE check should happen before ReactDOM bundle executes so that + // DevTools can report bad minification during injection. + checkDCE(); + module.exports = require('./cjs/react-dom.profiling.min.js'); +} else { + module.exports = require('./cjs/react-dom.development.js'); +} diff --git a/packages/next/src/compiled/react-dom/profiling.js b/packages/next/src/compiled/react-dom/profiling.js new file mode 100644 index 0000000000000..91f89f07ffe13 --- /dev/null +++ b/packages/next/src/compiled/react-dom/profiling.js @@ -0,0 +1,38 @@ +'use strict'; + +function checkDCE() { + /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ + if ( + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined' || + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE !== 'function' + ) { + return; + } + if (process.env.NODE_ENV !== 'production') { + // This branch is unreachable because this function is only called + // in production, but the condition is true only in development. + // Therefore if the branch is still here, dead code elimination wasn't + // properly applied. + // Don't change the message. React DevTools relies on it. Also make sure + // this message doesn't occur elsewhere in this function, or it will cause + // a false positive. + throw new Error('^_^'); + } + try { + // Verify that the code above has been dead code eliminated (DCE'd). + __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); + } catch (err) { + // DevTools shouldn't crash React, no matter what. + // We should still report in case we break this code. + console.error(err); + } +} + +if (process.env.NODE_ENV === 'production') { + // DCE check should happen before ReactDOM bundle executes so that + // DevTools can report bad minification during injection. + checkDCE(); + module.exports = require('./cjs/react-dom.profiling.min.js'); +} else { + module.exports = require('./cjs/react-dom.development.js'); +} diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 1439550ee4e30..49dbc1a53de19 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -743,6 +743,9 @@ const configSchema = { publicRuntimeConfig: { type: 'object', }, + reactProductionProfiling: { + type: 'boolean', + }, reactStrictMode: { type: 'boolean', }, diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 00b56e469a0b0..0fea822abd098 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -507,6 +507,12 @@ export interface NextConfig extends Record { */ optimizeFonts?: boolean + /** + * Enable react profiling in production + * + */ + reactProductionProfiling?: boolean + /** * The Next.js runtime is Strict Mode-compliant. * @@ -670,6 +676,7 @@ export const defaultConfig: NextConfig = { excludeDefaultMomentLocales: true, serverRuntimeConfig: {}, publicRuntimeConfig: {}, + reactProductionProfiling: false, reactStrictMode: false, httpAgentOptions: { keepAlive: true, diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 47390389a0018..290c951b8c601 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1760,7 +1760,6 @@ export async function copy_vendor_react(task_) { 'static.browser.js', 'unstable_testing.js', 'test-utils.js', - 'profiling.js', 'server.bun.js', 'cjs/react-dom-server.bun.development.js', 'cjs/react-dom-server.bun.production.min.js', diff --git a/test/integration/react-profiling-mode/app/client/page.js b/test/integration/react-profiling-mode/app/client/page.js new file mode 100644 index 0000000000000..2100efada9010 --- /dev/null +++ b/test/integration/react-profiling-mode/app/client/page.js @@ -0,0 +1,17 @@ +'use client' + +import React from 'react' + +export default function Page() { + return ( + { + window.profileResults = window.profileResults || [] + window.profileResults.push(res) + }} + > +

hello app client

+
+ ) +} diff --git a/test/integration/react-profiling-mode/app/favicon.ico b/test/integration/react-profiling-mode/app/favicon.ico new file mode 100644 index 0000000000000..4965832f2c9b0 Binary files /dev/null and b/test/integration/react-profiling-mode/app/favicon.ico differ diff --git a/test/integration/react-profiling-mode/app/layout.js b/test/integration/react-profiling-mode/app/layout.js new file mode 100644 index 0000000000000..a3a86a5ca1e12 --- /dev/null +++ b/test/integration/react-profiling-mode/app/layout.js @@ -0,0 +1,7 @@ +export default function Root({ children }) { + return ( + + {children} + + ) +} diff --git a/test/integration/react-profiling-mode/app/server/page.js b/test/integration/react-profiling-mode/app/server/page.js new file mode 100644 index 0000000000000..e3e37ad66bc7f --- /dev/null +++ b/test/integration/react-profiling-mode/app/server/page.js @@ -0,0 +1,9 @@ +import React from 'react' + +export default function Page() { + return ( + +

hello app server

+
+ ) +} diff --git a/test/integration/react-profiling-mode/next.config.js b/test/integration/react-profiling-mode/next.config.js new file mode 100644 index 0000000000000..aa4e620e4c11f --- /dev/null +++ b/test/integration/react-profiling-mode/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + reactProductionProfiling: true, +} diff --git a/test/integration/react-profiling-mode/pages/index.js b/test/integration/react-profiling-mode/pages/index.js index f8a1751572139..35d12375f088a 100644 --- a/test/integration/react-profiling-mode/pages/index.js +++ b/test/integration/react-profiling-mode/pages/index.js @@ -9,7 +9,7 @@ const Index = () => { window.profileResults.push(res) }} > -

hello world

+

hello pages

) } diff --git a/test/integration/react-profiling-mode/test/index.test.js b/test/integration/react-profiling-mode/test/index.test.js index d868cc3ddb62e..200530450970e 100644 --- a/test/integration/react-profiling-mode/test/index.test.js +++ b/test/integration/react-profiling-mode/test/index.test.js @@ -14,11 +14,22 @@ let app describe('React Profiling Mode', () => { describe('without config enabled', () => { beforeAll(async () => { + await fs.remove(nextConfig) await nextBuild(appDir) appPort = await findPort() app = await nextStart(appDir, appPort) }) - afterAll(() => killApp(app)) + afterAll(async () => { + await fs.writeFile( + nextConfig, + ` + module.exports = { + reactProductionProfiling: true + } + ` + ) + await killApp(app) + }) it('should not have used the react-dom profiling bundle', async () => { const browser = await webdriver(appPort, '/') @@ -30,29 +41,37 @@ describe('React Profiling Mode', () => { describe('with config enabled', () => { beforeAll(async () => { - await fs.writeFile( - nextConfig, - ` - module.exports = { - reactProductionProfiling: true - } - ` - ) await nextBuild(appDir, ['--profile']) appPort = await findPort() app = await nextStart(appDir, appPort) }) afterAll(async () => { - await fs.remove(nextConfig) await killApp(app) }) - it('should have used the react-dom profiling bundle', async () => { + it('should have used the react-dom profiling bundle for pages', async () => { const browser = await webdriver(appPort, '/') const results = await browser.eval('window.profileResults') expect(results.length).toBe(1) expect(results[0] && results[0][0]).toBe('hello') }) + + it('should have used the react-dom profiling bundle for client component', async () => { + const browser = await webdriver(appPort, '/client') + const results = await browser.eval('window.profileResults') + + expect(results.length).toBe(1) + expect(results[0] && results[0][0]).toBe('hello-app-client') + }) + + it('should have used the react-dom profiling bundle for server component', async () => { + // Can't test react Profiler API in server components but make sure rendering works + const browser = await webdriver(appPort, '/server') + + expect(await browser.waitForElementByCss('p').text()).toBe( + 'hello app server' + ) + }) }) })