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

Support react profiling option for app dir client components #51947

Merged
merged 2 commits into from
Jun 29, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -1933,6 +1941,7 @@ export default async function getBaseWebpackConfig(
...createRSCAliases(bundledReactChannel, {
reactSharedSubset: false,
reactDomServerRenderingStub: false,
reactProductionProfiling,
}),
},
},
Expand Down Expand Up @@ -1965,6 +1974,8 @@ export default async function getBaseWebpackConfig(
reactSharedSubset: true,
reactDomServerRenderingStub: true,
reactServerCondition: true,
// No server components profiling
reactProductionProfiling,
}),
},
},
Expand Down Expand Up @@ -2028,6 +2039,7 @@ export default async function getBaseWebpackConfig(
reactSharedSubset: true,
reactDomServerRenderingStub: true,
reactServerCondition: true,
reactProductionProfiling,
}),
},
},
Expand All @@ -2041,6 +2053,7 @@ export default async function getBaseWebpackConfig(
reactSharedSubset: false,
reactDomServerRenderingStub: true,
reactServerCondition: false,
reactProductionProfiling,
}),
},
},
Expand All @@ -2057,6 +2070,7 @@ export default async function getBaseWebpackConfig(
reactSharedSubset: false,
reactDomServerRenderingStub: false,
reactServerCondition: false,
reactProductionProfiling,
}),
},
},
Expand Down
38 changes: 38 additions & 0 deletions packages/next/src/compiled/react-dom-experimental/profiling.js
Original file line number Diff line number Diff line change
@@ -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');
}
38 changes: 38 additions & 0 deletions packages/next/src/compiled/react-dom/profiling.js
Original file line number Diff line number Diff line change
@@ -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');
}
3 changes: 3 additions & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,9 @@ const configSchema = {
publicRuntimeConfig: {
type: 'object',
},
reactProductionProfiling: {
type: 'boolean',
},
reactStrictMode: {
type: 'boolean',
},
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,12 @@ export interface NextConfig extends Record<string, any> {
*/
optimizeFonts?: boolean

/**
* Enable react profiling in production
*
*/
reactProductionProfiling?: boolean

/**
* The Next.js runtime is Strict Mode-compliant.
*
Expand Down Expand Up @@ -670,6 +676,7 @@ export const defaultConfig: NextConfig = {
excludeDefaultMomentLocales: true,
serverRuntimeConfig: {},
publicRuntimeConfig: {},
reactProductionProfiling: false,
reactStrictMode: false,
httpAgentOptions: {
keepAlive: true,
Expand Down
1 change: 0 additions & 1 deletion packages/next/taskfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
17 changes: 17 additions & 0 deletions test/integration/react-profiling-mode/app/client/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'

import React from 'react'

export default function Page() {
return (
<React.Profiler
id="hello-app-client"
onRender={(...res) => {
window.profileResults = window.profileResults || []
window.profileResults.push(res)
}}
>
<p>hello app client</p>
</React.Profiler>
)
}
Binary file not shown.
7 changes: 7 additions & 0 deletions test/integration/react-profiling-mode/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Root({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
9 changes: 9 additions & 0 deletions test/integration/react-profiling-mode/app/server/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

export default function Page() {
return (
<React.Profiler id="hello">
<p>hello app server</p>
</React.Profiler>
)
}
3 changes: 3 additions & 0 deletions test/integration/react-profiling-mode/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
reactProductionProfiling: true,
}
2 changes: 1 addition & 1 deletion test/integration/react-profiling-mode/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Index = () => {
window.profileResults.push(res)
}}
>
<p>hello world</p>
<p>hello pages</p>
</React.Profiler>
)
}
Expand Down
41 changes: 30 additions & 11 deletions test/integration/react-profiling-mode/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '/')
Expand All @@ -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')
huozhi marked this conversation as resolved.
Show resolved Hide resolved

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'
)
})
})
})