-
-
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
Perf rearchitecting #450
Perf rearchitecting #450
Conversation
6faae02
to
5473b4f
Compare
6eb0600
to
37eafdd
Compare
1 similar comment
@@ -16,7 +16,7 @@ const propTypes = forbidExtraProps({ | |||
day: momentPropTypes.momentObj, | |||
daySize: nonNegativeInteger, | |||
isOutsideDay: PropTypes.bool, | |||
modifiers: PropTypes.object, | |||
modifiers: PropTypes.instanceOf(Set), |
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's worth noting that this may make the PR semver-major, as this adds a requirement for es6-shimmed or above, and breaks IE <= 8 (if it wasn't already broken)
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 PR is already semver-major due to the fact that the DayPicker
now functions totally differently. Do you think this is a concern? I kind of think this project was not working with IE8 already.
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 long as the PR is already semver-major, and as long as we ensure that these requirements (Set
and Array.from
being globally polyfilled) are documented in the readme, I'm a solid 👍 on it.
src/components/CalendarDay.jsx
Outdated
const className = cx('CalendarDay', { | ||
'CalendarDay--outside': isOutsideDay, | ||
}, modifiersForDay.map(mod => `CalendarDay--${mod}`)); | ||
|
||
}, [...modifiers].map(mod => `CalendarDay--${mod}`)); |
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.from(modifiers, mod =>
CalendarDay--${mod})
is much much more efficient because it avoids the intervening 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.
okeedoke. Didn't know that syntax!
37eafdd
to
c3b5350
Compare
src/utils/isAfterDay.js
Outdated
export default function isAfterDay(a, b) { | ||
if (!moment.isMoment(a) || !moment.isMoment(b)) return false; | ||
|
||
const isSameYear = a.year() === b.year(); |
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.
You might be able to squeeze out a bit more perf by storing the results of x.year/month/date, depending on how expensive those calls are and how often this function is called.
src/utils/isDayVisible.js
Outdated
const firstDayOfFirstMonth = month.clone().startOf('month'); | ||
const lastDayOfLastMonth = month.clone().add(numberOfMonths - 1, 'months').endOf('month'); | ||
|
||
return !day.isBefore(firstDayOfFirstMonth) && !day.isAfter(lastDayOfLastMonth); |
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.
Should this use your isBeforeDay/isAfterDay utility methods?
src/utils/isDayVisible.js
Outdated
// TODO(maja): get this working for enableOutsideDays | ||
export default function isDayVisible(day, month, numberOfMonths) { | ||
const firstDayOfFirstMonth = month.clone().startOf('month'); | ||
const lastDayOfLastMonth = month.clone().add(numberOfMonths - 1, 'months').endOf('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.
You can avoid this work some of the time by doing an early return and putting this after it.
c3b5350
to
04a1e5e
Compare
04a1e5e
to
9220ddb
Compare
src/components/SingleDatePicker.jsx
Outdated
selected: day => this.isSelected(day), | ||
}; | ||
|
||
const initialVisibleMonthThunk = props.initialVisibleMonth || (() => (props.date || moment())); |
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 could be more efficient with props.initialVisibleMonth || (props.date ? () => props.date : () => moment())
(and even more so if the moment thunk was cached at module level instead of recreated every render)
src/components/SingleDatePicker.jsx
Outdated
const initialVisibleMonthThunk = props.initialVisibleMonth || (() => (props.date || moment())); | ||
const evaluatedMonth = initialVisibleMonthThunk(); | ||
const currentMonth = moment.isMoment(evaluatedMonth) ? evaluatedMonth : moment(); | ||
const visibleDays = getVisibleDays(currentMonth, props.numberOfMonths, props.enableOutsideDays); |
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, all this prop-dependent logic needs to be in both the constructor and componentWillReceiveProps
. Rather than duplicating it, is there some chunk of it that could be in a shared function?
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 think there is a problem with the this.props
/props
paradigm. Also do you mean that this needs to be reset when the month 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.
What I mean is, any prop-dependent logic in any constructor needs to be re-executed in componentWillReceiveProps
- so in this case, yes, when numberOfMonths
or enableOutsideDays
changes, visibleDays
and thus this.state.visibleDays
would need to be recomputed.
src/components/SingleDatePicker.jsx
Outdated
if (this.isTouchDevice || !hoverDate) return; | ||
|
||
let modifiers = {}; | ||
modifiers = this.deleteModifier(modifiers, hoverDate, 'hovered'); |
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.
const modifiers = this.deleteModifier({}, hoverDate, 'hovered');
?
altho this looks kind of weird, why the empty object?
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 empty object is so that when we call addModifier
/deleteModifier
the effects stack rather than overwriting each other. So for instance if something happens that causes two different modifiers to be added, we spread everything onto this object and then set the state at the end.
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.
Thanks, that makes sense.
src/utils/isAfterDay.js
Outdated
@@ -0,0 +1,18 @@ | |||
import moment from 'moment'; | |||
|
|||
export default function isAfterDay(a, b) { |
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 seems unfortunate to have both isAfterDay and isBeforeDay - would it be worth it to make one of the implementations (say this one) into return !isBeforeDay(a, b) && !isSameDay(a, b);
?
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.
we could also probs update usage to only have one. I just thought it was a bit clear to use one over the other in some cases.
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 don't mean, let's get rid of both - I mean, let's implement one in terms of the other.
src/utils/isDayVisible.js
Outdated
if (isBeforeDay(day, firstDayOfFirstMonth)) return false; | ||
|
||
const lastDayOfLastMonth = month.clone().add(numberOfMonths - 1, 'months').endOf('month'); | ||
if (isAfterDay(day, lastDayOfLastMonth)) return false; |
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.
return !isAfterDay(day, lastDayOfLastMonth);
?
src/utils/isInclusivelyBeforeDay.js
Outdated
|
||
export default function isInclusivelyBeforeDay(a, b) { | ||
if (!moment.isMoment(a) || !moment.isMoment(b)) return false; | ||
return a.isBefore(b) || isSameDay(a, b); | ||
return isBeforeDay(a, b) || isSameDay(a, b); |
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.
isn't this return !isAfterDay(a, b);
?
src/utils/isInclusivelyAfterDay.js
Outdated
|
||
export default function isInclusivelyAfterDay(a, b) { | ||
if (!moment.isMoment(a) || !moment.isMoment(b)) return false; | ||
return a.isAfter(b) || isSameDay(a, b); | ||
return isAfterDay(a, b) || isSameDay(a, b); |
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.
isn't this return !isBeforeDay(a, b);
?
b15a284
to
3c1bd25
Compare
@ljharb I think I addressed your concerns! Can you take a look? |
@@ -249,7 +629,7 @@ export default class DayPickerRangeController extends React.Component { | |||
|
|||
isDayAfterHoveredStartDate(day) { | |||
const { startDate, endDate, minimumNights } = this.props; | |||
const { hoverDate } = this.state; | |||
const { hoverDate } = this.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.
when would the state not be initialized to an empty object?
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.
When this function is called from the constructor, this.state has not yet been set.
... this same response applies on the other areas too.
It's a weird edge case, but this seemed like the cleanest solution? Like this will never be true in the constructor so I could also just return if this.state is not defined... or figure out some way to skip these modifiers
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.
gotcha (sorry for the multi-comment).
#450 (comment) is what i'd expect to be the solution
src/components/SingleDatePicker.jsx
Outdated
return { | ||
...updatedDays, | ||
...{ | ||
[monthIso]: { |
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 spread isn't necessary - you can just do:
return {
...updatedDays,
[monthISO]: {
...month,
[iso]: modifiers,
},
};
@@ -344,7 +620,8 @@ export default class SingleDatePicker extends React.Component { | |||
} | |||
|
|||
isHovered(day) { | |||
return isSameDay(day, this.state.hoverDate); | |||
const { hoverDate } = this.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.
when would the state not be initialized to an empty object?
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.
When this function is called from the constructor, this.state
has not yet been set.
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.
:-/ hmm - in that case it kind of seems like the meat of it should be extracted out to a pure function in a closure, and have both the constructor and isHovered
call into it with the data it needs.
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.
can you clarify that a little bit?
do you mean something like isHovered(day, hoverDate)
? I feel like that might be odd given that it'd suddenly be different from every other modifiers method
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.
Hmm - yeah, i can see how that'd be unnecessarily weird. I guess this is fine as-is.
3c1bd25
to
0b20ab4
Compare
Instead of passing down an object of modifier string => modifiers functions to each CalendarDay component and expecting the CalendarDay component to take care of updating itself, we now put the burden on the top of the tree instead of on the leaves. The previous model basically ended up meaning that whenever an interaction happened on an individual day, even a hover interaction, every single CalendarDay would have to recalculate its modifiers and rerender as a result. This was, as you can imagine, really god damn slow. In this new model, the DayPickerRangeController maintains a map with the following structure: ``` { MONTH_ISO_1: { DAY_ISO_1: Set(['modifer_1', 'modifier_2', ...]), DAY_ISO_2: Set(['modifer_1', 'modifier_2', ...]), ... }, ... } ``` It passes this down the tree such that each `CalendarMonth` and each `CalendarDay` only gets the information that pertains to it. This means that the updating of these modifiers is also handled at the top-level and is done in the `componentWillReceiveProps`, `onDayMouseEnter`, and `onDayMouseLeave` methods of the `DayPickerRangeController`. Fortunately, this allows us to more finely tune which days get updated and speeds up the rerendering/updating process dramatically.
0b20ab4
to
c95cbed
Compare
Hey guys, I have the following on a modifiers={{
selected: day =>
moment(this.props.selectedDay).isSame(day, 'day'),
'highlighted-calendar': day1 =>
this.props.highlightedDays.some(day2 =>
moment(day2).isSame(day1, 'day'),
),
}} |
Hey @dburles, can you open an issue for this so that we can have a convo on a separate thread? |
This PR is a dramatic rearchitecture of the way that we handle modifiers in
react-dates
on the whole.Instead of passing down an object of modifier string => modifiers functions to each CalendarDay component (one that gets rebuilt on every render call) and expecting the CalendarDay component to take care of updating itself, we now put the burden on the top of the tree instead of on the leaves. The previous model meant that whenever an interaction happened on an individual day, even a hover interaction, every single CalendarDay would have to recalculate its modifiers and rerender as a result. The idea was that an interaction on one day might have effects elsewhere in the calendar (like the
hovered-span
). The effect unfortunately was that react-dates was really god damn slow.In this new model, the DayPickerRangeController maintains a map with the following structure:
It passes this down the tree such that each
CalendarMonth
and eachCalendarDay
only gets the information that pertains to it. This means that the updating of these modifiers is also handled at the top-level and is done in thecomponentWillReceiveProps
,onDayMouseEnter
, andonDayMouseLeave
methods of theDayPickerRangeController
. Fortunately, this allows us to more finely tune which days get updated and speeds up the rerendering/updating process dramatically.I did some preliminary testing and saw the following improvements:
![screen shot 2017-04-13 at 10 35 39 pm](https://cloud.githubusercontent.com/assets/1383861/25154213/9d8ba8aa-2444-11e7-847a-1762f164ac6e.png)
Before:
After:
![screen shot 2017-04-13 at 10 55 41 pm](https://cloud.githubusercontent.com/assets/1383861/25154217/a688b0c4-2444-11e7-8214-58b2904eccc3.png)
I will update those graphs when everything is in place.
I still need to finish writing some tests, clean up some other tests, and also get this working with
enableOutsideDays
and theSingleDatePicker
. The bulk of the work is inDayPickerRangeController
right now, but I've been musing on ways to pull that in an HOC or something. SUGGESTIONS ARE WELCOME.In any case, I would appreciate a first review pass while I finish up the above points! <3
@lencioni @ljharb @moonboots @backwardok
UPDATE: Here are the final perf measurements!
![screen shot 2017-05-01 at 3 19 09 pm](https://cloud.githubusercontent.com/assets/1383861/25598878/97062454-2e8c-11e7-81d6-45555caa6652.png)
Before:
After:
![screen shot 2017-05-01 at 4 38 44 pm](https://cloud.githubusercontent.com/assets/1383861/25598893/accb5728-2e8c-11e7-94fe-dd52b5507e3f.png)
Here are the findings: