-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Conversation
426840c
to
3d90a46
Compare
handleDayClick(day, modifiers, e) { | ||
this.props.onDayClick(day, modifiers, e); | ||
onDayClick(day, e) { | ||
this.props.onDayClick(day, e); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 {...};
});
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}`} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 : () => {}; |
There was a problem hiding this comment.
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 : () => {}; |
There was a problem hiding this comment.
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)} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes of course!
There was a problem hiding this comment.
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}> |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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}> |
There was a problem hiding this comment.
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}> |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.
@majapw Are you still working on this? We just finished our implementation of I'm willing to test this and give feedback if you ship it up as |
@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? |
@sontek any place we're not using react-dates is because we haven't migrated it over yet (i.e., legacy) |
Hi @sontek! Sorry for the delay! I've been on vacation. I'll be returning to this this week and hopefully shipping it. :) |
3d90a46
to
89b3dac
Compare
@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! |
@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? |
89b3dac
to
8f59f88
Compare
{weeks.map((week, i) => ( | ||
<tr key={i}> | ||
{week.map((day, j) => { | ||
const modifiersForDay = getModifiersForDay(modifiers, day); |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
5a4c1b2
to
769ffda
Compare
Changes Unknown when pulling 769ffda on maja-perf-improvements into ** on master**. |
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and here
b89c194
to
d34207f
Compare
…resenting each month
…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.
…n you focus on one of the inputs
It was not especially being used, and this prevents the CalendarDay from being rerendered at every opportunity.
d34207f
to
df18385
Compare
Changes Unknown when pulling df18385 on maja-perf-improvements into ** on master**. |
Changes Unknown when pulling df18385 on maja-perf-improvements into ** on master**. |
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:
DRP
/SDP
no longer causes 4 renders (I had restructured theOutsideClickHandler
logic when the tether was introduced and this basically reverts to the pre-tether world).DateInput
only has anonFocus
action instead of anonFocus
action on the input and anonClick
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.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).CalendarMonth
component also now keeps track of the day moment objects used for its children in its state and only updates them as necessary.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:
After:
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