-
Notifications
You must be signed in to change notification settings - Fork 61
Virtualize the emoji category listing to avoid performance hit on initial render #50
Conversation
…tial render Refs #45
This eliminates the need for CellMeasurer, which slightly improves performance and allows us to move styling back to the CSS file.
…e shape of the return value
This eliminates the need for CellMeasurer, which slightly improves performance and allows us to move styling back to the CSS file.
Hey @ahutchings - just a quick note to say that these changes look like they're definitely in a positive direction, I'll try and give them a thorough looking over before the end of the week. |
Great idea @ahutchings. Just a note that react-addons-shallow-compare is deprecated. You can use PureComponent instead. |
PureComponent was introduced in react v15.3.0, so we can't use it just yet unless we want to drop v14 support from emoji picker. However, we'll still have a transitive dependency on shallow-compare through react-virtualized until they get a chance to upgrade. The react-virtualized maintainer has said that he will look at upgrading to PureComponent once react v16 drops. |
I just discovered that the react-virtualized styles that we depend on are all inline, so emojione-picker users will not need to include react-virtualized's stylesheet. I'll push a new commit shortly to remove the link to it from the preview html. |
Great idea @ahutchings. If this lands in master, I'll definitely try it again :) Imo, it's always great to support the last 2 versions of React. So keeping React 14 for now might be best, even though I believe most users now use React 15. If React 16 lands before this PR, maybe you'll be able to also drop react 14 support here :) |
This pull request is in pretty good shape now and should be safe to review. It's been working well in our testing, so I don't intend to push any more commits other than in response to feedback. |
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.
@@ -0,0 +1,30 @@ | |||
export default function createEmojisFromStrategy(strategy) { |
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.
Nice, thanks for moving this out of the component 💯
src/utils/createRowsSelector.js
Outdated
@@ -0,0 +1,71 @@ | |||
import {chunk, map, values} from 'lodash'; |
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.
Could you import lodash methods individually to reduce bundle size? I.e in the form:
import chunk from 'lodash/chunk';
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.
Done.
src/categories.jsx
Outdated
@@ -0,0 +1,163 @@ | |||
import React, {PropTypes, Component} from 'react'; | |||
import {AutoSizer, List} from 'react-virtualized'; | |||
import {findIndex, throttle} from 'lodash'; |
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 re: lodash imports
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.
Done.
src/categories.jsx
Outdated
import EmojiRow from './emoji-row'; | ||
import Modifiers from './modifiers'; | ||
|
||
const CATEGORY_HEADER_ROW_HEIGHT = 46; |
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.
Lets add a note here that these need keeping in sync with the CSS file
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.
Done.
src/emoji-row.jsx
Outdated
role="option" | ||
key={emoji.unicode} | ||
onClick={function(e) { | ||
onChange(emoji); |
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're defining functions in a loop here within the render, so this is not going to be performant. Looking back I see it was like this before so I could go ahead and address this from a PR after this lands if you prefer 😄
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 would you prefer to do this? The change handlers need to be bound to to the emoji object being selected, so I believe we'll at least need to allocate at least one function per emoji.
One possibility -
The onClick and onKeyUp handlers are both intended to select the emoji, so we could define the onClick and onKeyUp handlers in the Emoji component instead, and the component rendering the Emoji component could pass a single onSelect prop instead. That should save one function allocation per emoji, since the onClick and onKeyUp functions would be shared on the Emoji prototype. It would also have the benefit of consolidating the keyup key filtering logic, because the Emoji component is rendered by two different components.
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.
One way would be to only pass the onChange
callback to Emoji
. And inside emoji do what you need to do, while binding the function only once in the constructor. (Same thing for the keyUp).
It's funny how many problems I can spot in my own code from just 6 months ago… Thankfully you tackled many of them 😆 |
Most of the review comments are addressed now - just had one question about the direction you want to go with src/emoji-row.jsx. |
Move the lower-level onClick and onKeyUp handlers down into the Emoji component, and expose a single onSelect prop from Emoji.
src/emoji-row.jsx
Outdated
if (e.which === 13 || e.which === 32) { | ||
onChange(emoji); | ||
} | ||
onSelect={() => { |
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 also do onSelect={this.props.onChange}
and keep the Emoji component the responsability to send the right information.
The difference will be that instead of creating a function for each emoji for each render, you'll be creating one function for each emoji only.
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 Emoji component is also used in the category list in the header, where the parent component needs the category id instead of the emoji object when the Emoji component is selected. I decided to keep the contract as is since it doesn't assume anything about what the parent component will do when the Emoji is selected. Performance is still good since updates are prevented at the row level.
I'm working on putting together a |
This isn't ready for a merge just yet, but I wanted to open this pull request to get some feedback on the direction and integrate any changes you'd like to make.
These changes address #45, which describes the momentary lag that you get when first opening the emoji picker. This lag is caused by two things:
react-virtualized
to only render the emoji that are needed based on scroll position. This ensures that only one to three categories are rendered at any given time.One caveat of this from a code maintenance perspective is that all of the styles that affect the category layout need to be inline or pulled in via some other means of style modularization. react-virtualized'sCellMeasurer
component uses a trick of pre-rendering categories elsewhere in the DOM in order to measure their size, effectively breaking their inheritance-based styling that was applied via the CSS file. The react-virtualized docs go into a little more detail describing the how and why. I've worked around this for now by putting all of the category styles inline, leaving the rest of the styles in the CSS file. However, I think this situation should be addressed with a more cohesive solution before merging.Update: I've pushed an update that no longer uses CellMeasurer, which gives a small perf improvement and also means that all styles can continue to be defined in the stylesheet instead of inline. There is a small amount of duplication here between the stylesheet and the component that calculates the category height - this will need to be kept in sync. If any category-layout-affecting style properties are overridden by the application this would throw react-virtualized's positioning off, so this is probably worth documenting unless there is some other workaround.
There are a couple additional steps that dependent applications will need to take to use this new version. I will update the README with this information once we've answered some of the questions about how to package and distribute the new version.
react-virtualized's CSS needs to be loaded by the app. This is located in react-virtualized's package atUpdate: The stylesheet will not be needed, see my later comment.react-virtualized/styles.css
. We could inline these styles intocss/picker.css
, but I'd be concerned about the styles getting out of sync. The react-virtualized-select project recommends loading the CSS files separately. Do you have any thoughts around this?react-addons-shallow-compare
needs to be installed as an app dependency.