Skip to content

Instantly share code, notes, and snippets.

@bvaughn
Last active April 3, 2024 07:41
Show Gist options
  • Save bvaughn/8de925562903afd2e7a12554adcdda16 to your computer and use it in GitHub Desktop.
Save bvaughn/8de925562903afd2e7a12554adcdda16 to your computer and use it in GitHub Desktop.
Interaction tracing with React

This API was removed in React 17


Interaction tracing with React

React recently introduced an experimental profiler API. After discussing this API with several teams at Facebook, one common piece of feedback was that the performance information would be more useful if it could be associated with the events that caused the application to render (e.g. button click, XHR response). Tracing these events (or "interactions") would enable more powerful tooling to be built around the timing information, capable of answering questions like "What caused this really slow commit?" or "How long does it typically take for this interaction to update the DOM?".

With version 16.4.3, React added experimental support for this tracing by way of a new NPM package, scheduler. However the public API for this package is not yet finalized and will likely change with upcoming minor releases, so it should be used with caution.

This Gist provides some high-level documentation for anyone looking to experiment with the API.

WARNING: "unstable_" APIs may change in any version without respecting semver

React has always respected semantic versioning for its public API. However, sometimes we want to share an early preview of something that is likely to change in the future. This is one of those cases. If you care about semantic versioning guarantees, don't use "unstable_" APIs because they are subject to change in any patch release. If you use them, you agree that things will break between releases, and that you use a lockfile to avoid that.

Note that if you're using a version of react/react-dom that's less than 16.6, you should refer to this earlier revision of the documentation instead.

Note that if you're using the schedule package v0.3.0-v0.4.0 you should refer to this earlier revision of the documentation instead.

Overview

An interaction is a descriptive string (e.g. "login button clicked") and a timestamp. When you declare an interaction, you also provide a callback in which to do some work related to that interaction. Interactions are similar to "zones" in that any work done within the "zone" is attributed to the interaction.

For example, you can trace a React render like so:

import { unstable_Profiler as Profiler } from "react";
import { render } from "react-dom";
import { unstable_trace as trace } from "scheduler/tracing";

trace("initial render", performance.now(), () =>
  render(
    <Profiler id="Application" onRender={onRender}>
      <Application />
    </Profiler>,
    domElement
  )
);

In the above example, the profiler's onRender prop will be called after the application renders and mounts. Among other things, it will receive a parameter that specifies the Set of interactions that were part of that render (i.e. the "initial render" interaction).

The interaction will also appear in the React DevTools profiler plug-in.

API

The following methods are a subset of the interaction tracing API.

unstable_trace

unstable_trace(
  name: string,
  timestamp: number,
  callback: () => T
) => T

Traces a new interaction (by appending to the existing set of interactions). The callback function will be executed and its return value will be returned to the caller. Any code run within that callback will be attributed to that interaction. Calls to unstable_wrap() will schedule async work within the same zone.

unstable_wrap

unstable_wrap(callback: Function) => Function

Wrap a function for later execution within the current interaction "zone". When this function is later run, interactions that were active when it was "wrapped" will be reactivated.

The wrapped function returned also defines a cancel() property which can be used to notify any traceed interactions that the async work has been cancelled.

Examples

Tracing initial render

import { unstable_trace as trace } from "scheduler/tracing";

trace("initial render", performance.now(), () => render(<Application />));

Tracing state updates

import { unstable_trace as trace } from "scheduler/tracing";

class MyComponent extends Component {
  handleLoginButtonClick = event => {
    trace("Login button click", performance.now(), () => {
      this.setState({ isLoggingIn: true });
    });
  };

  // render ...
}

Tracing async work

import {
  unstable_trace as trace,
  unstable_wrap as wrap
} from "scheduler/tracing";

trace("Some event", performance.now(), () => {
  setTimeout(
    wrap(() => {
      // Do some async work
    })
  );
});
@raphaelbs
Copy link

@bvaughn thanks for the comprehensive response!

If you mean "has no more scheduled work pending"

I did, yeah!

In hindsight you could just look at the last commit and when it took place vs the current time

The PoC I’m working on is exactly that! But I wonder if user inputs will affect this e.g. if the user interacts right away, its interactions would account as part of the “initial mounting” tracing bucket.

then the experimental interaction tracing API could be used to do this but for your purposes

In this scenario I would trace individual points of the application to be used as “stop profiling” flags?

A "cascading update" means something in the commit phase (componentDidMount, componentDidUpdate, useEffect, or useLayoutEffect) scheduled another render right after React just finished the current one.

What qualifies the right after? Is it a fixed timestamp?
In React’s point of view, there’s a difference between an user triggered render vs a cascading updated one?

@octopitus
Copy link

CMIIW but I think the example of "tracing async work" must be something like this:

import {
  unstable_trace as trace,
  unstable_wrap as wrap
} from "scheduler/tracing";

trace("Some event", performance.now(), () => {
  const wrapped = wrap(() => {
    // Do some async work
  })

  setTimeout(() => wrapped());
});

wrap must be called within the same "zone" of trace. The wrapped function also need to be called.

@bvaughn
Copy link
Author

bvaughn commented Apr 26, 2020

@octopitus That's not really different from what the current example shows, since wrap is invoked synchrously.

@octopitus
Copy link

octopitus commented Apr 27, 2020

I have a demo follow your example but it doesn't work. Turns out the thing wrapped in setTimeout never get call.

@bvaughn
Copy link
Author

bvaughn commented Apr 27, 2020

@octopitus Your demo has a mistake:

trace("api", performance.now(), () => {
  setTimeout(() => { // This wrapper function is the mistake
    wrap(() => {
      setMount(true);
    });
  });
});

Compare to the example above:

trace("Some event", performance.now(), () => {
  setTimeout( // There's no wrapper function
    wrap(() => {
      // Do some async work
    })
  );
});

@octopitus
Copy link

Oh, right, my bad. Thanks for pointing out.

@rodchen-king
Copy link

I have one question, Any help? Why "No interactions were recorded"?

import React, { useEffect } from 'react';
import { render } from "react-dom";
import { unstable_trace as trace, unstable_Profiler as Profiler } from "scheduler/tracing";
import OtherComponent from './OtherComponent'

const MyComponent = () => {
  const callback = (
      id,
      phase, 
      actualDuration, 
      baseDuration, 
      startTime, 
      commitTime, 
      interactions 
    ) => {
      console.log(`id: ${id}`)
      console.log(`phase: ${phase}`)
      console.log(`actualDuration: ${actualDuration}`)
      console.log(`baseDuration: ${baseDuration}`)
      console.log(`startTime: ${startTime}`)
      console.log(`commitTime: ${commitTime}`)
      console.log(`interactions: ${JSON.stringify(interactions)}`)

  }

  const useDidMount = fn => useEffect(() => fn && fn(), []);

  useDidMount(() => {
    trace("initial render", performance.now(), () =>
      render(
        <Profiler id="Navigation" onRender={callback}>
          <OtherComponent key='1' />
        </Profiler>,
        document.getElementById('li3')
      )
    );
  })

  return (
    <div>
      <ul>
        <li>1</li>
        <li>2</li>
        <li id='li3'></li>
      </ul>
    </div>
  );
}

export default MyComponent;

@0xdevalias
Copy link

0xdevalias commented Nov 18, 2020

I'm also seeing "No interactions were recorded" in the devtools when I try and use this :(

Any thoughts?

  • react-dom: 16.13.1
  • scheduler: 0.19.1
  • React Developer Tools: 4.10.0 (revision 11a2ae3a0d) (11/12/2020)

Note that if you're using a version of react/react-dom that's less than 16.6, you should refer to this earlier revision of the documentation instead.

Note that if you're using the schedule package v0.3.0-v0.4.0 you should refer to this earlier revision of the documentation instead.

@Khizarkhan07
Copy link

I'm seeing "No interactions were recorded" in the devtools when I try and use this :(

Any thoughts?

"react": "^16.8.6"
"scheduler": "^0.20.1",

@shackra
Copy link

shackra commented Dec 3, 2020

I"m on the same boat with no interactions were recorded

  • "scheduler": "^0.20.1",
  • "react": "^17.0.1",
  • "react-context-devtool": "^2.0.0",

@emmanuelpotvin
Copy link

Same here

@jviall
Copy link

jviall commented Feb 11, 2021

no interactions were recorded with

  • scheduler: 0.19.1 (also tried 0.20.1 but noticed react-dom used 0.19.1)
  • react: 16.14.0
  • react-dom: 16.14.0
  • React Dev Tools: 4.10.1

@MitchelSt
Copy link

MitchelSt commented Mar 27, 2021

How would I go over using async (wrap) using a async/await try-catch?

  const onClick = () => {
    trace('clicked "fetch" button', performance.now(), async () => {
      try {
        const { data } = await axios(
          "https://jsonplaceholder.typicode.com/users/1"
        );
        setData(data);
      } catch (error) {
        setError(error);
      }
    });
  };

@MartinKristof
Copy link

Interactions option in Devtools does not exist. Tracing interactions does not work too -> schedule 0.4.0.

@leviathan264
Copy link

leviathan264 commented Jan 10, 2024

Anyone know the status of this or what the current recommend practice is? Any sort of unstable_trace or <Profiler/> stats are not showing up for me in the Profiler tab in latest version of React Developer Tools.

    "react-dom": "17.0.2",
    "scheduler": "0.20.2"

@kentcdodds
Copy link

This feature has been removed from React and the DevTools. I've not seen anything to suggest they'll return.

@bvaughn
Copy link
Author

bvaughn commented Jan 10, 2024

This header text is the very top of the page.
Screenshot 2024-01-10 at 3 22 44 PM

@rajatrawataku1
Copy link

Since the API has been removed, is there any other way to achieve this currently? We are using React 18, and we want to track the total re-rendering time resulting from a particular interaction. In our case, the component gets re-rendered multiple times, leading to significant jank, but the profiler numbers are less compared to the actual jank experienced.

So, the total re-rendering time (including all re-renders) after one interaction is 500ms, but the profiler data shows individual re-render times, which are not as relevant.

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