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

Performance Improvements #217

Merged
merged 5 commits into from
Jan 25, 2017
Merged

Performance Improvements #217

merged 5 commits into from
Jan 25, 2017

Conversation

majapw
Copy link
Collaborator

@majapw majapw commented Dec 19, 2016

I'm marking this as breaking due to the removal of the modifiers prop from CalendarDay. It's a fairly minor change all in all, because it's not really being used at all throughout the library, but nevertheless breaking.

This does a number of small performance improvements:

  1. Opening the DRP/SDP no longer causes 4 renders (I had restructured the OutsideClickHandler logic when the tether was introduced and this basically reverts to the pre-tether world).
  2. The DateInput only has an onFocus action instead of an onFocus action on the input and an onClick on the input's wrapper. Previously, when we were disabling the input on touch screens, this was a necessary action. However, readonly inputs do still receive focus events on touch devices afaict so this is no longer necessary.
  3. The CalendarMonthGrid component now keeps track of the month moment objects used for its children in its state and only updates them as necessary (instead of creating new moment objects on every render).
  4. The CalendarMonth component also now keeps track of the day moment objects used for its children in its state and only updates them as necessary.
  5. The CalendarDay no longer takes a modifiers prop. It was only using these props in its user interaction callbacks and it was hard not to rerender each time as the modifiers array was being reinstantiated each time.

I used react-addons-perf to track the improvements. The following wasted time printouts are from 5 seconds of basic interaction with the component.

Before:
screen shot 2016-12-19 at 8 28 04 am

After:
screen shot 2016-12-19 at 8 26 58 am

There's likely still some work that can be done, but this seems like a good start.

Should help with #178 and #140

to: @airbnb/webinfra @ljharb @lencioni

@majapw majapw added the semver-major: breaking change A non-backwards-compatible change; anything that breaks code - including adding a peerDep. label Dec 19, 2016
@majapw majapw force-pushed the maja-perf-improvements branch 2 times, most recently from 426840c to 3d90a46 Compare December 19, 2016 14:55
@coveralls
Copy link

Coverage Status

Coverage decreased (-1.6%) to 84.916% when pulling 3d90a46 on maja-perf-improvements into 08906d6 on master.

handleDayClick(day, modifiers, e) {
this.props.onDayClick(day, modifiers, e);
onDayClick(day, e) {
this.props.onDayClick(day, e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal, but it'd be good to destructure out the prop and then call it, so that its this value won't be this.props (same throughout)

const hasNumberOfMonthsChanged = this.props.numberOfMonths !== numberOfMonths;
if (hasMonthChanged || hasNumberOfMonthsChanged) {
if (initialMonth.isAfter(this.props.initialMonth)) {
months.shift();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mutating the state array directly, rather than using setState - is this intentional? Can we avoid the mutation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely needs to be changed. You should use the callback version of setState:

this.setState((previousState) => {
  ...
  return {...};
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by using that callback @lencioni? How is that more helpful than just calling setState?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setState is async, so if you need to modify state based on the previous state, you need to use the callback version of setState otherwise it may end up doing an incorrect thing, which could be a subtle cause of bugs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, if you don't need anything from the previous state, then you don't need to use the callback version.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in componentWillReceiveProps and is directly mutating the current state which might be a problem, but is doing so based on the change in props not on a change in state.

I don't think we need the callback, but I can figure out a way to do this with setState instead of mutating the state under the hood.

(i >= firstVisibleMonthIndex) && (i < firstVisibleMonthIndex + numberOfMonths);
return (
<CalendarMonth
key={`CalendarMonth-${i}`}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This key just needs to be i - ie, only unique within the array.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought we were against using indices as keys

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this would not include the index at all and would be based off of unique datae. But what you have here is functionally equivalent to a plain index with unnecessarily added complexity of the string.

@@ -263,32 +265,36 @@ export default class DateRangePicker extends React.Component {
onFocusChange,
} = this.props;

const onOutsideClick = !withPortal && !withFullScreenPortal ? this.onOutsideClick : () => {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be more performant to cache this noop function outside the render path, so react can know when it's the same object. Alternatively, set it to null/undefined, and it won't be passed at all?

@@ -325,27 +325,31 @@ export default class SingleDatePicker extends React.Component {

const dateString = this.getDateString(date);

const onOutsideClick = !withPortal && !withFullScreenPortal ? this.onOutsideClick : () => {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here re noop vs undefined

onMouseUp={(e) => this.onDayMouseUp(day, e)}
onClick={(e) => this.onDayClick(day, e)}
onTouchStart={(e) => this.onDayTouchStart(day, e)}
onTouchEnd={(e) => this.onDayTouchEnd(day, e)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are going for performance, you might also want to consider binding these event handlers in the constructor, and then re-binding only when this.props.day changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh actually, that's a good point - they can be bound in the constructor and simply look up this.props.day every time, so there's no need for any rebinding.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes of course!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(they actually could have done this in the first place, too)

});

return (
<div className={calendarMonthClasses} data-visible={isVisible}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated: in addition to data-visible (or maybe instead of it?) we might want to use aria-hidden.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not in this PR, but you are correct


<tbody className="js-CalendarMonth__grid">
{weeks.map((week, i) =>
<tr key={i}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array index keys should generally be avoided. I think it might be okay in this case, but it would still be better to use something that identifies the week object instead if that is possible.

}, modifiersForDay.map(mod => `CalendarMonth__day--${mod}`));

return (
<td className={className} key={j}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here regarding array index and j.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on CalendarMonthGrid suggests that this is preferable and acceptable. What is the change you are proposing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

day is some sort of moment object, right? I'd go with something like this key={day.dayOfYear()}. I think it might be more important for the tr to have a better key than it is for this td to have a better key, but you might as well make them all better at the same time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not always a moment object. Sometimes it is null and also @ljharb mentioned that these only have to be unique within the array, no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, key only needs to be unique within the array. That said, when it is a moment object, making it something actually unique and tied to the row might make things more performant, even if some of the items use an index.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since some of the day variables may equal null, how do you propose using something different for the moment objects and not having it collide with the keys for the null values? I think having it tied to day of week makes the most sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Day of the week, and index, seem like they wouldn't conflict - alternatively, 'day_' + dayNumber wouldn't collide either.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

j is already day of the week, like it goes from 0 - 6. I guess i don't understand what the problem is with this key...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in that case maybe the only problem is the variable name :-)

}

shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much benefit does this give you now that you've moved the moment objects to state?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still diminishes the render count significantly so I think it is meaningful to leave this.

const hasNumberOfMonthsChanged = this.props.numberOfMonths !== numberOfMonths;
if (hasMonthChanged || hasNumberOfMonthsChanged) {
if (initialMonth.isAfter(this.props.initialMonth)) {
months.shift();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely needs to be changed. You should use the callback version of setState:

this.setState((previousState) => {
  ...
  return {...};
});

@@ -95,11 +115,22 @@ export default class CalendarMonthGrid extends React.Component {
this.props.onMonthTransitionEnd();
}

getMonths(initialMonth, numberOfMonths) {
let month = initialMonth.clone().subtract(1, 'month');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this function is written, it is a little difficult to grok. To avoid doing mental acrobatics, what do you think about moving this into your loop? It shouldn't be too hard if methods like add or subtract can take negative numbers.

@sontek
Copy link

sontek commented Dec 28, 2016

@majapw Are you still working on this? We just finished our implementation of react-dates but are holding off on shipping it live because of the bad performance even on iphone6. I think this PR might solve that for us

I'm willing to test this and give feedback if you ship it up as @next or something as well

@sontek
Copy link

sontek commented Dec 28, 2016

@majapw Also, I noticed airbnb just uses another picker on mobile. Is that because of performance or were there other criteria they used when doing that?

Wondering if I should be following the same workflow?

@ljharb
Copy link
Member

ljharb commented Dec 28, 2016

@sontek any place we're not using react-dates is because we haven't migrated it over yet (i.e., legacy)

@majapw
Copy link
Collaborator Author

majapw commented Jan 12, 2017

Hi @sontek! Sorry for the delay! I've been on vacation. I'll be returning to this this week and hopefully shipping it. :)

@coveralls
Copy link

Coverage Status

Coverage decreased (-1.6%) to 85.534% when pulling 89b3dac on maja-perf-improvements into f931a82 on master.

@sontek
Copy link

sontek commented Jan 22, 2017

@majapw No worries on the delay, everyone deserves a vacation! :) I'm just excited to see work on the performance since its the only thing holding us back on using this!

@sontek
Copy link

sontek commented Jan 23, 2017

@majapw Would it make sense to break up some of these changes into smaller pieces so it'll be easier to get some of the changes into a release?

@coveralls
Copy link

Coverage Status

Coverage decreased (-1.5%) to 85.694% when pulling 8f59f88 on maja-perf-improvements into dd5def0 on master.

{weeks.map((week, i) => (
<tr key={i}>
{week.map((day, j) => {
const modifiersForDay = getModifiersForDay(modifiers, day);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how much you guys like comments from randoms on your PRs, but I'm just wondering: as I mentioned in #245, is it possible to run the calculations for days conditionally? As I understand it, even months not currently displayed are having their modifiers calculated. Is there any reason this isn't being done just for visible months? Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't run the calculations for non-visible months, you get weird behavior for transitions. For example, if part of the selected range is in the month before or the month after, it will look unselected during the transition and then flash to the selected teal color.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would visible months ±1 on either side work then? I don't imagine many use cases would want to display more than 3 calendars at once, and running the calculations for 5 calendars sounds more appealing than all 12.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is the current behavior @timhwang21

@majapw majapw force-pushed the maja-perf-improvements branch 2 times, most recently from 5a4c1b2 to 769ffda Compare January 25, 2017 20:51
@majapw
Copy link
Collaborator Author

majapw commented Jan 25, 2017

@ljharb @lencioni I have addressed all comments. Can one of you give me a stamp? I'd like to get this merged in today.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling 769ffda on maja-perf-improvements into ** on master**.

@majapw
Copy link
Collaborator Author

majapw commented Jan 25, 2017

Almost all of the additions to the code are in lifecycles methods. I think I'm going to invest some time into getting the full mount tests working, but I'll probably merge through this coveralls check.

@@ -186,7 +192,7 @@ export default class DateRangePicker extends React.Component {
} = this.props;
const { dayPickerContainerStyles } = this.state;

const onOutsideClick = !withFullScreenPortal ? this.onOutsideClick : undefined;
const onOutsideClick = !withFullScreenPortal && withPortal ? this.onOutsideClick : undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine but not wrapping the condition in parens makes me nervous.

@@ -263,32 +265,36 @@ export default class DateRangePicker extends React.Component {
onFocusChange,
} = this.props;

const onOutsideClick = !withPortal && !withFullScreenPortal ? this.onOutsideClick : undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here

@@ -276,7 +276,7 @@ export default class SingleDatePicker extends React.Component {
selected: day => this.isSelected(day),
};

const onOutsideClick = !withFullScreenPortal ? this.onClearFocus : undefined;
const onOutsideClick = !withFullScreenPortal && withPortal ? this.onClearFocus : undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

@@ -336,27 +336,31 @@ export default class SingleDatePicker extends React.Component {

const dateString = this.getDateString(date);

const onOutsideClick = !withPortal && !withFullScreenPortal ? this.onOutsideClick : undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

@coveralls
Copy link

Coverage Status

Coverage decreased (-2.4%) to 84.753% when pulling 769ffda on maja-perf-improvements into d01cd54 on master.

@majapw majapw force-pushed the maja-perf-improvements branch 2 times, most recently from b89c194 to d34207f Compare January 25, 2017 22:05
Maja Wichrowska added 5 commits January 25, 2017 14:16
…ndarMonth

Previously, all these day moment objects were being created every time the CalendarMonth was updated. Now, only those CalendarDay objects that have changed are getting updated.
It was not especially being used, and this prevents the CalendarDay from being rerendered at every opportunity.
@coveralls
Copy link

Coverage Status

Changes Unknown when pulling df18385 on maja-perf-improvements into ** on master**.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling df18385 on maja-perf-improvements into ** on master**.

@coveralls
Copy link

Coverage Status

Coverage decreased (-2.4%) to 84.815% when pulling df18385 on maja-perf-improvements into 1cbc4db on master.

@majapw majapw merged commit 351eb5a into master Jan 25, 2017
@majapw majapw deleted the maja-perf-improvements branch January 25, 2017 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver-major: breaking change A non-backwards-compatible change; anything that breaks code - including adding a peerDep.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants