Skip to content

Instantly share code, notes, and snippets.

@bvaughn
Last active November 14, 2024 19:13
Show Gist options
  • Save bvaughn/25e6233aeb1b4f0cdb8d8366e54a3977 to your computer and use it in GitHub Desktop.
Save bvaughn/25e6233aeb1b4f0cdb8d8366e54a3977 to your computer and use it in GitHub Desktop.
How to use profiling in production mode for react-dom

Table of Contents

Profiling in production

React DOM automatically supports profiling in development mode for v16.5+, but since profiling adds some small additional overhead it is opt-in for production mode. This gist explains how to opt-in.

Create React App

Enabling profiling permanently

At the moment, the only way to permanently enable production profiling in CRA apps is to eject. Then you can follow the instructions below and apply these changes to config/webpack.config.prod.js in your app folder.

However, you can also enable profiling temporarily without ejecting.

Enabling profiling temporarily

If you only want to profile the application locally in production mode, you can do this by editing node_modules directly.

Follow the instructions below, and apply them to node_modules/react-scripts/config/webpack.config.prod.js. Then you can run yarn build or npm run build to get a profiling build. Note that your changes would be temporary and will not persist between re-runs of your package manager.

Changing the Webpack config

To enable profiling in production mode, modify Webpack configuration file (config/webpack.config.prod.js) as shown below.

react-dom@^16.5.0 / schedule@^0.4.0

module.exports = {
  // ...
  resolve: {
    // ...
    alias: {
      // ...
      'react-dom': 'react-dom/profiling',
      'schedule/tracking': 'schedule/tracking-profiling',
    },
    // ...
  },
  // ...
};

react-dom@^16.5.0 / [email protected]

Note that if you're using the schedule package v0.3.0 you should declare the following alias instead:

module.exports = {
  // ...
  resolve: {
    // ...
    alias: {
      // ...
      'react-dom': 'react-dom/profiling',
      'schedule/tracking': 'schedule/cjs/schedule-tracking.profiling.min'
    },
    // ...
  },
  // ...
};

Optional: Disabling mangling for local profiling

When profiling locally, you might want to disable function name mangling so that you can see the component names in the profiler. Note that this will significantly increase your bundle size so only do this during local development! To do this, find the mangle option for UglifyJSPlugin in the config, and set it to false. Don't forget to undo your changes before a real deployment.

Webpack 4

If you are using Webpack 4 to bundle your apps, add the following import aliases to your production config:

react-dom@^16.5.0 / schedule@^0.4.0

module.exports = {
  //...
  resolve: {
    alias: {
      'react-dom': 'react-dom/profiling',
      'schedule/tracking': 'schedule/tracking-profiling',
    }
  }
};

react-dom@^16.5.0 / [email protected]

Note that if you're using the schedule package v0.3.0 you should declare the following alias instead:

module.exports = {
  //...
  resolve: {
    alias: {
      'react-dom': 'react-dom/profiling',
      'schedule/tracking': 'schedule/cjs/schedule-tracking.profiling.min'
    }
  }
};

Optional: Disabling mangling for local profiling

When profiling locally, you might want to disable function name mangling so that you can see the component names in the profiler. Note that this will significantly increase your bundle size so only do this during local development! To do this, find the mangle option for UglifyJSPlugin in the config, and set it to false. Don't forget to undo your changes before a real deployment.

Troubleshooting

Tracking broke after an NPM update

Both your application and react-dom need to use the same schedule version in order for tracking to work. NPM may install multiple copies if the versions don't match, in which case your application will end up tracking interactions with a difference package than react-dom reads them from.

The safest way to ensure that this does not happen is to copy the exact schedule version that react-dom specifies as a dependency.

Fixing this with NPM

For example, assuming you are using react-dom version 16.5.0 you can find which version of schedule to use by running:

➜ npm view [email protected]

dependencies:
loose-envify: ^1.1.0  object-assign: ^4.1.1 prop-types: ^15.6.2   schedule: ^0.3.0  

The above output shows that [email protected] depends on schedule@^0.3.0, so your application code will also want to use that exact version.

If you're not sure if mismatching versions are installed, you can use npm ls schedule to check:

➜ npm ls schedule
[email protected] /Users/bvaughn/Desktop/test-schdeule
├─┬ UNMET DEPENDENCY [email protected]
│ └── UNMET DEPENDENCY [email protected] 
├─┬ UNMET DEPENDENCY [email protected]
│ └── UNMET DEPENDENCY [email protected] 
└── UNMET DEPENDENCY schedule@^0.2.0

In the above example react-dom and my application are depending on different versions of schedule. This will result in multiple copies of the package being installed (one in node_modules/schedule and another in node_modules/react-dom/node_modules/schedule).

You can fix this by updating your app to use the exact version react-dom is using:

➜  npm i schedule@^0.3.0
➜  npm ls schedule      
[email protected] /Users/bvaughn/Desktop/test-schdeule
├─┬ [email protected]
│ └── [email protected]  deduped
├─┬ [email protected]
│ └── [email protected]  deduped
└── [email protected] 

Fixing this with Yarn

Yarn users can check to see if mismatching versions are installed using yarn why:

➜ yarn why schedule       
[1/4] 🤔  Why do we have the module "schedule"...?
[2/4] 🚚  Initialising dependency graph...
[3/4] 🔍  Finding dependency...
[4/4] 🚡  Calculating file sizes...
=> Found "[email protected]"
info Has been hoisted to "schedule"
info This module exists because it's specified in "dependencies".
=> Found "react-dom#[email protected]"
info This module exists because "react-dom" depends on it.
=> Found "react#[email protected]"
info This module exists because "react" depends on it.
✨  Done in 0.70s.

You can fix this by updating your app to use the exact version react-dom is using:

➜  yarn add schedule@^0.3.0
➜  yarn why schedule
[1/4] 🤔  Why do we have the module "schedule"...?
[2/4] 🚚  Initialising dependency graph...
[3/4] 🔍  Finding dependency...
[4/4] 🚡  Calculating file sizes...
=> Found "[email protected]"
info Has been hoisted to "schedule"
info Reasons this module exists
   - Specified in "dependencies"
   - Hoisted from "react-dom#schedule"
   - Hoisted from "react#schedule"
@kentcdodds
Copy link

I'm not sure when, but it looks like Create React App combined the prod and dev configs (finally!) and the config can now be found at: ./node_modules/react-scripts/config/webpack.config.js. Also, it now uses TerserPlugin and you can fix the component name mangling by setting keep_classnames and keep_fnames to true.

I just wrote a blog post about this: Profile a React App for Performance

Thanks for this gist (and everything else) @bvaughn!

@amadeu01
Copy link

@bvaughn Does it work with RN?

@gassiss
Copy link

gassiss commented Dec 6, 2019

In case anyone has the same issue, I tried npm run build --profile and got the standard build. npm run build -- --profile did the trick.

Thanks for your hard work by the way :)

@bvaughn
Copy link
Author

bvaughn commented Feb 6, 2020

Thanks for the note, @gassiss. I updated the README to fix the NPM syntax.

@seanders
Copy link

@bvaughn Wondering if you had any thoughts on this question.

@bvaughn To clarify, does this only have a performance impact when you're profiling? Or is that performance impact always there?

Put another way:
Does aliasing react-dom for react-dom/profiling on its own introduce a performance hit, or is it only limited to invocations of the Profiler component, specifically.

i.e. Would two identical react apps, aside from using react-dom vs. react-dom/profiling, see any difference in performance?

@bvaughn
Copy link
Author

bvaughn commented May 12, 2020

Performance impact is minor: 4 additional fiber attributes, some additional calls to peformance.now() within profiled subtrees.

The memory impact of the additional fiber attributes should be minor, but will always be there if you're loading the profiling build- so only load it if you're using the data.

The calls to peformance.now() aren't done unless you're within a part of the tree wrapped with a <Profiler> tag so the CPU impact- minimal either way- would not apply to parts of your app that aren't being profiled.

@seanders
Copy link

@bvaughn Excellent. Thanks for the clear, concise, and thorough explanation. You are making many a dev happy on my team, right now.

@abhilash-u
Copy link

will it work for react native apps as well ?

@bvaughn
Copy link
Author

bvaughn commented May 26, 2020

Profiler API supports React Native.

Aliasing methods described in this gist are only for React DOM. You'll want to reach out to the React Native team to find out what their recommended aliasing approach is.

@pjisha
Copy link

pjisha commented Jun 28, 2020

I get this error displayed while trying to profile my react app. Profiling support requires either a development or production-profiling build of React v16.5+.

Versions of react and react-dom in my package.json is 16.8.2. And webpack is 4.16.5
What is that I am missing?

@usrbowe
Copy link

usrbowe commented Aug 17, 2020

Support for React Native

I was able to use babel module-resolver plugin and get Profiling renderer to work with this alias config:
https://gist.github.com/usrbowe/cf0fe2bf4e73923059d06bb442d054f9#file-babelrc-L7-L8

Do not forget to --reset-cache when starting packager. Also do check for any transform errors, since sometimes depending on project folder structure, path to renderer might be different.

@amadeu01, @abhilash-u

@wintercounter
Copy link

I have the opposite problem. My prod build always has profiling. Any advice where to look? Everything else is prod in the CI tool chain, and they work fine, Terser, Prod only Webpack plugins, all good. Checked webpack-bunde-analyzer and react-dom.production.min.js is loaded.

@AhsanAyaz
Copy link

Was anyone able to find it out if it works for ReactNative using the solution provided by @ursbowe?
@amadeu01, @abhilash-u

It doesn't work for me though.

@lukasz-app
Copy link

lukasz-app commented Dec 7, 2020

@usr

Support for React Native

I was able to use babel module-resolver plugin and get Profiling renderer to work with this alias config:
https://gist.github.com/usrbowe/cf0fe2bf4e73923059d06bb442d054f9#file-babelrc-L7-L8

What version of react-native are you using?

@AhsanAyaz
I couldn't get it to work with the above snippet. I think it is because at the bottom of the node_modules/react-native/Libraries/Renderer/shims/ReactNative.js require depends on the relative path

ReactNative = require('../implementations/ReactNativeRenderer-prod');

To make it work change this line to:

ReactNative = require('../implementations/ReactNativeRenderer-profiling')

This change, being inside node_modules, will not persist after npm/yarn install, but you can keep it using patch-package

@issacgerges
Copy link

issacgerges commented Mar 25, 2021

@bvaughn did y'all consider an explicit enabled/disabled / shouldSample prop in this <Profiler> so it could be sampled (or enabled only at certain times) without impacting the component hierarchy at all?

@larry-dalmeida
Copy link

larry-dalmeida commented Jul 9, 2021

Performance impact is minor: 4 additional fiber attributes, some additional calls to peformance.now() within profiled subtrees.

The memory impact of the additional fiber attributes should be minor, but will always be there if you're loading the profiling build- so only load it if you're using the data.

The calls to peformance.now() aren't done unless you're within a part of the tree wrapped with a <Profiler> tag so the CPU impact- minimal either way- would not apply to parts of your app that aren't being profiled.

This is great @bvaughn! Thank you and love the work! ❤️

I want to profile some components and not all in production, aggregate and send that data to a monitoring endpoint.
I understand that using the profiler in production should have a small or negligible impact on user experience at least for profiling 20-50 (guess) components on a page.

I would like to know if there are any more known data/estimates (perhaps from tests at facebook) on the performance impact on the user experience when shipping the production build of react including the profiler?

I'm hoping for some numbers, if possible (even if guesstimates) to try and determine if it's negligible enough to be shipped to all customers until we have data and then make a call on if we should invest in shipping a different build to smaller customer traffic (bit complex due to our setup)?

I understand if that's not available, I suppose the best way to find out would be to test and gather numbers for ourselves in production.

@xushimin
Copy link

xushimin commented Dec 2, 2021

image
image

Profiling not support in a development of React v16.13.0

@bvaughn
Copy link
Author

bvaughn commented Dec 3, 2021

@xushimin Not sure what you're seeing there but it's most likely a transient error. Close DevTools, reload the page and reopen.

@LongLiveCHIEF
Copy link

So how do we enabling profiling if we're not using CRA?

@haase1020
Copy link

I have the same question as @LongLiveCHIEF . I am not using CRA but would like to enable profiling to troubleshoot errors that are occurring in the production build only.

@M1M0G
Copy link

M1M0G commented Mar 27, 2022

I have the same problem, when using non-СRA, changing the alias in the webpack config has no effect

@tonyhallett
Copy link

tonyhallett commented May 14, 2022

for those using Parcel you can put the alias in package.json

"alias": {
      "react-dom": "react-dom/profiling",
      "scheduler/tracing":"scheduler/tracing-profiling"
    }

for those wanting it to be applied conditionally with an environment variable the typescript code below can be used for a custom Resolver.

ReactProfilingAliasResolver.ts

import {Resolver} from '@parcel/plugin';
import {PluginLogger, Dependency} from '@parcel/types'
import NodeResolver, { FilePath, Options, Environment, ResolverContext, Nullable, ResolveResult } from '@parcel/node-resolver-core';

const reactDom = 'react-dom';
const schedulerTracing = 'scheduler/tracing';

const logOrigin = 'ReactProfilingAliasResolver';

function createDiagnosticWithOrigin(message:string){
  return {
    message,
    origin:logOrigin
  }
}

const shouldLog = false
function log(logger:PluginLogger,resolved:Nullable<ResolveResult>, dependency:Dependency){
  if (shouldLog) {
    logger.log(createDiagnosticWithOrigin(`parent - ${dependency.resolveFrom!}`));
      
    if(resolved){
      logger.log(createDiagnosticWithOrigin(`resolved ${dependency.specifier} ${JSON.stringify(resolved)}`));
    }else{
      logger.log(createDiagnosticWithOrigin(`failed to resolve ${dependency.specifier}`));
    }
  }
}

function shouldProfile(specifier:string){
  return process.env.REACT_PROFILING !== undefined && (specifier === reactDom || specifier === schedulerTracing);
}

export default new Resolver({
  async resolve(arg) { 
    const {specifier, dependency,options} = arg;
    const logger = arg.logger;

    if (shouldProfile(specifier)){

      const resolver = new ReactAliasingNodeResolver({
        fs: options.inputFS,
        projectRoot: options.projectRoot,
        // Extensions are always required in URL dependencies.
        extensions:
          dependency.specifierType === 'commonjs' ||
          dependency.specifierType === 'esm'
            ? ['ts', 'tsx', 'js', 'jsx', 'json']
            : [],
        mainFields: ['source', 'browser', 'module', 'main'],
        packageManager: options.shouldAutoInstall
          ? options.packageManager
          : undefined,
        logger,
      });
  
      const resolved = await resolver.resolve({
        filename: specifier,
        specifierType: dependency.specifierType,
        parent: dependency.resolveFrom,
        env: dependency.env,
        sourcePath: dependency.sourcePath,
        loc: dependency.loc,
      });
      
      log(logger,resolved, dependency);
      return resolved;
    }
    return null;
  }
});


class ReactAliasingNodeResolver extends NodeResolver{
  constructor(opts:Options){
    super(opts)
  }

  async loadAlias(_filename:string, _sourceFile:FilePath, _env:Environment,_ctx:ResolverContext): Promise<Nullable<NodeResolver.ResolvedAlias>> {
    const resolved = filename == reactDom ? 'react-dom/profiling' : 'scheduler/tracing-profiling';
    return {
      type:'file',
      sourcePath:'', // not used
      resolved
    }
  }

}

node-resolver-core.ts

declare module '@parcel/node-resolver-core' {
    import type {FileSystem} from '@parcel/fs';
    import type {PackageManager} from '@parcel/package-manager';
    import type {PluginLogger, ResolveResult} from '@parcel/types';
    export type {ResolveResult} from '@parcel/types';

    export type Nullable<T> = T | undefined | null;

    export type FilePath = string;
    

    // to type if required
    export type URLSearchParams = unknown
    export type ResolverContext = unknown
    export type Environment = unknown

    export type Module = {
        moduleName?: string,
        subPath?: Nullable<string>,
        moduleDir?: FilePath,
        filePath?: FilePath,
        code?: string,
        query?: URLSearchParams,
    };
    export type Options = {
        fs: FileSystem,
        projectRoot: FilePath,
        extensions: Array<string>,
        mainFields: Array<string>,
        packageManager?: PackageManager,
        logger?: PluginLogger,
    };
    export type ResolveArg = {
        filename:string,
        parent,
        specifierType,
        env,
        sourcePath,
        loc,
    }
    export type ResolvedAlias = {
        type: 'file' | 'global',
        sourcePath: FilePath,
        resolved: string,
    };
    class NodeResolver {
        projectRoot: FilePath
        constructor(opts: Options) {}
        resolve(resolveArg:ResolveArg):Promise<Nullable<ResolveResult>> 
        loadAlias(filename:string, sourceFile:FilePath, env:Environment,ctx:ResolverContext):Promise<Nullable<ResolvedAlias>>
        findNodeModulePath(filename:string,sourceFile:FilePath,ctx:ResolverContext):Module | null | undefined
    }
    export = NodeResolver
}
  1. Create a package for the resolver ( compile to js )
  2. Include in .parcelrc
{
  "extends": "@parcel/config-default",
  "resolvers": ["parcel-resolver-react-profiling", "..."]
}

npm script usage ( windows ) - see build-profile

{
  "scripts": {
    "start": "parcel src/html/debug/parcel-dev.index.html",
    "build-prod": "parcel build src/html/build/index.html --dist-dir dist/build",

    "build-profile": "set REACT_PROFILING=true&& parcel build src/html/profiling/index.html --dist-dir dist/debug" 
  }
}

Note that as there is currently no invalidateOnEnvChange for resolvers the result will be cached and re-used even if you don't set the environment variable.
This is not an issue when you always want to always profile a specific production build that has a different cache key ( entries and mode ). In the example above build-prod and build-profile have a different entry. ( build-profile index.html has a script for standalone react-devtools to enable profiling of a WebView2 ).
If it is an issue then no-cache.

At some point I will add the resolver to npm, but in the meantime here it is.

@Tymek
Copy link

Tymek commented Jun 7, 2022

in Vite putting this in vite.config.ts worked:

export default defineConfig({
// ...
    resolve: {
        alias: {
            'react-dom': path.resolve(
                __dirname,
                'node_modules/react-dom/profiling'
            ),
            'scheduler/tracing': path.resolve(
                __dirname,
                'node_modules/scheduler/tracing-profiling'
            ),
        },
    },
// ...

@louiszawadzki
Copy link

louiszawadzki commented Sep 26, 2022

For versions of React Native equal to or greater than 0.61, I improved @lukaszchopin's solution so you don't have to use patch-package. You can use this configuration in your babel.config.js:

const path = require('path');

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    [
      'module-resolver',
      {
        root: ['./'],
        resolvePath(sourcePath) {
          if (sourcePath === '../implementations/ReactNativeRenderer-prod') {
            return path.resolve(
              __dirname,
              './node_modules/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-profiling',
            );
          }
          return undefined;
        },
      },
    ],
  ],
};

@in-op
Copy link

in-op commented Dec 21, 2022

@bvaughn, you still need the scheduler aliases, at least when using webpack bundling alone, which may be the source of some confusion here for people having trouble getting it working. I tested with the exact versions of react-dom and scheduler, and if you skip the scheduler alias you get this error:

It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. Your bundler might have a setting for aliasing both modules. Learn more at https://reactjs.org/link/profiling

Technically I got a minified error that linked here, since it's a production build.

@goosewobbler
Copy link

@tonyhallett Your code didn't work but I made a module which does fix this for Parcel. Turned out to be a lot less code than I expected:

https://github.com/goosewobbler/parcel-resolver-react-profiling

@athulantony95
Copy link

I am getting the below error for your solution @louiszawadzki

error: index.js: Cannot find module 'babel-plugin-module-resolver'
Require stack:
- app/root/node_modules/metro-react-native-babel-transformer/node_modules/@babel/core/lib/config/files/plugins.js
- app/root/node_modules/metro-react-native-babel-transformer/node_modules/@babel/core/lib/config/files/index.js
- app/root/node_modules/metro-react-native-babel-transformer/node_modules/@babel/core/lib/index.js
- app/root/node_modules/metro-react-native-babel-transformer/src/index.js
- app/root/node_modules/metro-transform-worker/src/index.js
- app/root/node_modules/metro/src/DeltaBundler/Worker.js
- app/root/node_modules/jest-worker/build/workers/processChild.js

@ahayes91
Copy link

@Tymek thanks for the vite snippet! Wondering if you ever saw errors like this during your vite build:

ERROR: [vite:load-fallback] Could not load /Users/aislinn.hayes/code/monorepo/apps/app1/node_modules/react-dom/profiling/test-utils (imported by ../../node_modules/@testing-library/react/dist/@testing-library/react.esm.js): ENOENT: no such file or directory, open '/Users/aislinn.hayes/code/monorepo/apps/app1/node_modules/react-dom/profiling/test-utils'
✘ [ERROR] Could not read from file: /Users/aislinn.hayes/code/monorepo/apps/app1/node_modules/react-dom/profiling

    ../../node_modules/@testing-library/react/dist/@testing-library/react.esm.js:3:21:
      3 │ import ReactDOM from 'react-dom';
        ╵                      ~~~~~~~~~~~

✘ [ERROR] Could not read from file: /Users/aislinn.hayes/code/monorepo/apps/app1/node_modules/react-dom/profiling/test-utils

    ../../node_modules/@testing-library/react/dist/@testing-library/react.esm.js:1:27:
      1 │ import * as testUtils from 'react-dom/test-utils';
        ╵                            ~~~~~~~~~~~~~~~~~~~~~~

✘ [ERROR] Could not read from file: /Users/aislinn.hayes/code/monorepo/apps/app1/node_modules/react-dom/profiling/client

    ../../node_modules/@testing-library/react/dist/@testing-library/react.esm.js:4:32:
      4 │ import * as ReactDOMClient from 'react-dom/client';

I don't know why @testing-library/react would even be trying to build during our production build 🤨
We exclude any files that reference that package from our tsconfig.json AFAIK.
For extra complexity, I am using vite in a monorepo as well 😇

    "react": "18.2.0",
    "react-dom": "18.2.0",
    "vite": "5.1.4",

Any pointers on where to look to try to debug this one? Thank you!

@zeorin
Copy link

zeorin commented Mar 15, 2024

{
  resolve: {
    alias: [
      { find: /^react-dom$/, replacement: 'react-dom/profiling' },
      { find: 'scheduler/tracing', replacement: 'scheduler/tracing-profiling' }
    ]
  }
}

in vite.config.js did the trick for me.

@ThenMorning
Copy link

ThenMorning commented Jun 12, 2024

I am using react-devtools-core to implement a custom backend for debugging a custom renderer similar to React Native. During the debugging process, React is using the development build version:
"react": "18.2.0", "react-devtools-core": "4.28.0", "react-reconciler": "0.29.0"
However, I have found that in react-devtools-core, the recordProfilingDurations method is not capturing accurate durations for the fiber nodes, and all durations are 0. Are there any additional steps required to record the render times (duration) accurately?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment