张大侠

React Snippets: Debug Component Performance with ES7 Annotations

张大侠 · 2016-12-14推荐 · 122阅读 原文链接

During our Neos code sprint in Hamburg, we devoted a day to optimizing the rendering performance of our Neos UI package. Performance optimization can be a tricky issue, but thanks to the transparent data flow in React, it can be done in a very fine-grained way. Since our backend is a complex React application with use cases like extensibility, i18n and theming, we use React/Redux to its fullest extent. This includes usage of advanced features like the React context.

Step 1: Finding the culprit

We were experiencing nasty delays on (seemingly) simple changes in the UI. Using React's new component profiling mechanism, we analyzed our rendering tree and stumbled upon some very deep nesting. Since we try to be as efficient as possible and use shouldComponentUpdate with shallow comparison as well as selector memoization with reselect as much as possible, this seemed weird to us.

Screenshot of React's performance profiling - before

These big spikes in the rendering tree were making us suspicious.

Screenshot of React's performance profiling - before

The EditorEnvelope component was the target of our investigation.


Step 2: Why the h… did you update?

Looking closer, we decided to investigate why our EditorEnvelope component was updating, because we did not expect it to do so at this point. In theory, finding out why a React component updated is dead simple: either a part of the state it listens to has changed or its props have changed. We could rule out a state change quickly using the Redux dev tools, but finding out what exactly changed in a component’s props can be a bit harder, especially if you wrap components with decorators to give them additional capabilities, as we do for our extensibility use case. We added a few console.log() statements to shouldComponentUpdate, but the resulting output wasn’t really satisfying in terms of readability. So we decided to build a little tool to see exactly what changed in a component’s state, props, or context. This would allow us to use this tool to debug future performance issues much more easily.

We used an ES7-style decorator to wrap the shouldComponentUpdate function and get nice console output. Since a function decorator in ES7 has access to the object’s scope, we were able to extract the values of this.props, this.state and this.context to compare them with the new values passed to the decorated shouldComponentUpdate, and output any changes in a structured and readable way.

const debugReasonForRendering = (targetReactComponent, key, descriptor) => {
  const originalShouldComponentUpdate = descriptor.value;            

  // Important: do NOT use an arrow function here, otherwise “this““ will be bound to the wrong context.
  descriptor.value = function (nextProps, nextState, nextContext) {
    const differencesInProps = findDifferences('props', this.props, nextProps);
    const differencesInState = findDifferences('state', this.state, nextState);
    const differencesInContext = findDifferences('context', this.context, nextContext);
    if (differencesInProps.length || differencesInState.length || differencesInContext.length) {
      // not shown: nice console output for all differences in state, props or context
    }

    // call the original shouldComponentUpdate with the original arguments
    return originalShouldComponentUpdate.apply(this, arguments);
  };
  return descriptor;
};

This, when applies to shouldComponentUpdate, produced a neat and readable output that didn't spam the console.

@debugReasonForRendering
shouldComponentUpdate(...args) {
    return shallowCompare(this, ...args);
}

Screenshot of the console output of our reason-for-update helper

Screenshot of the console output of our reason-for-update helper


Step 3: Fixing the problem

Using this little helper, we were able to quickly determine what part of the props changed and, with some refactoring, were able to fix the problem. How exactly this worked will be subject of another blog post (since it is a pattern that we will likely adopt project-wide). Here’s what the timeline looked like after the optimization. As you can see, the part of the tree below EditorEnvelope is now gone, which means that shouldComponentUpdate did its job correctly. Also, rendering that component now takes about 0.5 ms instead of over 3 ms.

Fixed React rendering screenshot

Fixed React rendering


Step 4: Making it public

We’ve published our little helper for everyone to enjoy and will keep updating it (e.g. to support React’s PureComponent, which doesn’t expose shouldComponentUpdate to subclasses). Check it out on npm.

PS: If you want to get in touch with us and discuss these exciting things, there's a Neos Barcamp on Saturday in Hamburg where we'd love to discuss this with you! If you use the discount code corefriend, the ticket is less than 30 €.

Check out the debugging module on NPM

Register for the next barcamp

相关文章