facebook / react

The library for web and native user interfaces.

Home Page:https://react.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

DevTools: Profiling tool improvements umbrella

bvaughn opened this issue · comments

I met with @lahmatiy this afternoon to chat about his project react-render-tracker. Here are a few pain points mentioned during the meeting:

  • Not clear when a component/tree is unmounted/remounted.
  • DevTools typically displays data for a component in current update (e.g. the latest) but doesn't represent changes over time.
  • It isn't always clear why something updates (e.g. why did context change higher up, what was the flow of props/state above that resulted in a new prop)
  • It's difficult to detect when effects re-run unexpectedly due to dependency problems. (Nothing really reports this.)

This is an umbrella issue for some ideas I wrote down during the discussion that might be good additions to the React DevTools/Profiler. The items listed below may be worked on independently– and some may turn out to be not worth doing (but they seem worth discussing and considering).

Legacy profiler

Track changed values

The Profiler currently has an opt-in setting for detecting why something re-rendered, but all this does is show the name of the state/prop that changed. Should we also add a setting to display the changed value?

We shied away from doing this for a long time because:

  • We didn't want to incur the cost of serializing deep object structures during a profiling session.
  • Without eagerly serializing an object, we don't have a solution for mutable values. (We can't serialize these later since they may have changed.)

Maybe there are some ways to address the above concerns though? For instance, we could reduce the amount of data we needed to eagerly serialize by:

  • Only serialize values that changed between renders. (This would avoid the mutable object scenario.)
  • Only serialize parts of an object that changed. (See here for possible precedent.)

Perhaps this, combined with making the setting opt-in, would enable us to add this feature.

Explicitly display unmounts

The DevTools profiler only displays components that were committed (e.g. visible on the screen at the point in time when work was committed), but React also spends time unmounting components– and perhaps an unmount isn't expected in some cases (e.g. when a wrapper object is added and React deeply unmounts and remounts a tree).

Should we show some sort of rollup (e.g. at the commit level) for components that were un-mounted too? This way they wouldn't be invisible an easy to overlook.

Viewing changes for a subtree across time

The Components tab allows you to explore the entire application tree, or double-click to drill into what we call the "owners tree"– the list of things rendered by a particular component (the things it "owns").

Maybe the Profiler could provide a similar method to drill into a component (e.g. double-clicking it) to then provide a snapshot of how that component changed over time? This might make it easier to discover things like unmounts/remounts within a known scope. (Or maybe we could also show other component-level summary stats when in this mode?)

Scheduling profiler

Explicit unmount/mount markers

Should we add component un-mounts/mounts as explicit marks in the new profiler? Profiling tools currently focus on the time spent rendering (or perhaps mounting) a component, but unmounts are somewhat hidden. (Perhaps more importantly, unexpected remounts are not highlighted enough.)

Should we also add a setting to display the changed value?

What would be the take-away knowing the before and after value? I feel like (strong emphasis on "feel") that most of the unintended updates are due to a value being updated that only changed due to referential equality e.g. functions and objects. And it's unlikely that a prop changed from a primitive to a complex value. So I suspect that knowing what prop changed is usually enough to recognize that the value needs memoization.

So does my action really change knowing the value before and after?

Maybe for starters we can experiment with tracking type changes first and see if people need even more granular data? At least for me seeing that some prop repeatedly changes and that it's an object or function gives me actionable advise. And if that object or function is already memoized I can look at the dependencies of that useMemo and useCallback and track it further up until I find the unmemoized value. At the same time, I wouldn't be able to do much about changes of e.g. string values.

(Perhaps more importantly, unexpected remounts are not highlighted enough.)

When you talk about remounts does this only concern mount/unmount of the same element type or would this also highlight mounts/remounts in the same "place" (if that is something the Profiler knows about) i.e. mounts/unmounts of different element types. For example, a common beginner mistake is defining components during render which means they (and their subtrees) are remounted on every render (which is especially problematic if their host components contain state such as being focused):

function App() {
  const input = styled.input({ color: 'blue' });

	return <input />
}

Being able to highlight these with devtools may help avoid these mistakes or even notice previously unnoticed instances.

What would be the take-away knowing the before and after value?

Not sure, but it seems to be a commonly requested feature. (Maybe @lahmatiy can weigh in here?)

Maybe for starters we can experiment with tracking type changes first and see if people need even more granular data? At least for me seeing that some prop repeatedly changes and that it's an object or function gives me actionable advise.

TBH I think a type change seems pretty unlikely, so I'd be hesitant to add code to track that.

When you talk about remounts does this only concern mount/unmount of the same element type or would this also highlight mounts/remounts in the same "place"...a common beginner mistake is defining components during render which means they (and their subtrees) are remounted on every render

When I wrote the issue, I was thinking more about the use case of wrapping a part of the tree (which causes a deep unmount and remount) but you're right that inline functions are another common cause. Ideally this would highlight both cases!

What would be the take-away knowing the before and after value?

fwiw, this is exactly what the Redux DevTools do. You can see:

  • the Redux action object that caused the state to be updated in the first place
  • the diff of the old state vs new state
  • the current state after the dispatch was complete

All of those have value depending on what you're trying to do.

Related to this, I would love to know what the initial cause was of a React render in the first place - what components were the first ones queued, what the actual state updates were in those, etc.

I'd agree that as amazing as the React DevTools are right now, the biggest thing they're missing is the view of state+components over time. I realize that tracking that info may be difficult, but if you can figure out some ways to address that functionality, it could be hugely helpful.

and yes, co-sign the idea of figuring out a way to notify users or highlight when components keep getting blown away due to defining types while rendering.

Related to this, I would love to know what the initial cause was of a React render in the first place - what components were the first ones queued

FWIW, this is something that's partly available in the Profiler UI already:
Screen Shot 2021-09-09 at 9 32 26 AM

The bit that's missing is drilling into exactly what changed (if it's a hooks change):
Screen Shot 2021-09-09 at 9 33 09 AM

Thanks @bvaughn for the great notes and spotlight some topics to discuss!

When I wrote the issue, I was thinking more about the use case of wrapping a part of the tree (which causes a deep unmount and remount) but you're right that inline functions are another common cause. Ideally this would highlight both cases!

React render tracker (RRT) covers both cases. For example:

import * as React from "react";
import ReactDOM from "react-dom";

function Content() {
  return "some content";
}

function Wrapper({ children }) {
  return children;
}

function Case1() {
  const [count, setCount] = React.useState(0);
  const StyledComponent = function () {
    return <Content />;
  };

  return (
    <div>
      <StyledComponent />
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
    </div>
  );
}

function Case2() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      {count % 2 ? (
        <Wrapper>
          <Content />
        </Wrapper>
      ) : (
        <Content />
      )}
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
    </div>
  );
}

ReactDOM.render([<Case1 />, <Case2 />], document.getElementById("root"));

Here is the video how RRT handles it (note that RRT shows component's tree as owner-based hierarchy by default, that's why Wrapper and Content on the same level in the beginning):

Screen.Recording.2021-09-09.at.22.00.46.mov

the biggest thing they're missing is the view of state+components over time

That's a general functionality for RRT to show what's was changed for a component over time. It doesn't contain all the state of a component (because it consumes a lot of memory) but a simple diff. In case of changed state or a context, it's usually a trigger for update (re-render) and RRT marks such updates with lightning icon. Here is an example:

telegram-cloud-photo-size-2-5321065061126616305-y

Here is the video how RRT handles it (note that RRT shows component's tree as owner-based hierarchy by default, that's why Wrapper and Content on the same level in the beginning):

Just to clarify, this is showing changes for only the subtree rendered by the currently selected component? (The "owners tree" to use React DevTools terminology.)

@bvaughn RRT component's tree showing the entire tree for a render root. Plans to add it is possible to limit what to show, like React Devtools owner tree but additionally to see entire component's subtree or subtree with selected types of components. In a real app there are a lot of components, most of them are not significant for an understanding what's going on. The simplest solution is to provide the user with a selection of the components that matter. But I think there must be a way to define what is worth showing and what is not. But these are still crazy ideas. I don't think owners tree is solving the problem.

I see. I asked because (in my opinion) there is no way such a UI/UX scales for an app larger than a small, demo application.

Hi folks!
I apologize that there was no news from me for a long time. I have a lot of updates that have happened in the React Render Tracker over the past time.
First of all, let me share slides I crafted recently React Render Tracker – Overview & Instructions (PDF version in case icloud doesn't work for you).

Updates

Diff improvements

Values diff was improved. For instance for objects RRT takes a sample of 3 entries max (usually it's more than enough) to show some details in changes. The rest changes are shown as a comment how many more entries are different. A "shallow equal" badge was added for objects and arrays when no changes in entries or elements, so objects/arrays are different by a reference only.

Example of improved diff

Size of payload

The size of payload (data collecting from React internals and transferring to UI) depends on the type of app. Approximate its 2,5-3,5K events per 1MB. A payload size can be observed in status bar:

RRT statusbar

Considering that a lot of new events and data have been added recently, and there have been no size optimizations yet, this is not so much. I believe the size can be reduced.

Tracking for call stack traces and locations of hook usage

As you might see on a screenshot above, RRT is collecting call stack trace for hook usage in diffs now, as well as custom hook invocations. That's not only for state hooks (useState/useReducer), but also for context and memo hooks (useMemo/useCallback)

Improve tracking for update reasons

Tracking for Component#setState(), useState() / useReducer() hooks callback and Component#forceUpdate() were added. In most cases RRT may detect a location of such calls. There are two more update reasons (if I haven't missed anything) which are not supported by RRT, but I'm seeking a way to implement them.

image

Added tracking for update bailouts

It's was a little bit tricky, but turned out possible to track update bailouts. Such information is useful for understanding for which component, why and how often the wave of updates stops. Bailout updates can be observed on fiber's tree as count badge and as an event on events log:

Event log with update bailouts

Most developers are surprised at the topic of update bailouts and that there is something else besides React.memo() and shouldComponentUpdate().

image

Info sections

Added more sections for a selected component to observe props updates, memoization effectiveness and context usage. Let me just attach screenshots of the slides as they are more informative:

image
image
image

Conclusion

As I found, there are much more useful things which can be gathered from React internals than modern React tools provide. React is not friendly for devtools, so we need to use various hacks and tricks to get the information we need. Sometimes even hacks and tricks don't work. However, in most cases it's possible and that's much more than nothing. I would be happy if React's core will be tweaked to avoid hacks for data gathering, or at least to add more possibilities in data collecting.

Nevertheless, I continue to work on getting data from React internals and visualizing it (a lot of things to do), showing what this can give us as developers of React apps. I am aware that React internals based solutions may break in future React releases. But I believe this can be fixed, or new versions of React can be released so that they can reproduce the same functionality, if that functionality is of value to developers. I get positive feedback from developers who were able to find and fix problems in their applications using the features described above. Other tools did not help with this. I think this is a good sign.

Once I done with my experiments, I'm going to describe which things might be changed in React to make data gathering simpler (without hacks) or just possible. For now you can take a look at source of RTT (mostly in those modules: one, two) to see which approaches I used to make features described above possible.