Skip to content
This repository has been archived by the owner on Jan 31, 2023. It is now read-only.

Virtualize the emoji category listing to avoid performance hit on initial render #50

Merged
merged 18 commits into from
Feb 12, 2017
Merged

Conversation

ahutchings
Copy link
Contributor

@ahutchings ahutchings commented Feb 1, 2017

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:

  1. First, if you are not using a sprite sheet, then an HTTP request is made for each rendered emoji. The requests are bottlenecked by the browser's per-origin concurrent request limits. We can update the README to more strongly recommended the sprite sheet approach and perhaps pull in some more examples of how to use the emojione-provided SVG/PNG sprite sheets.
  2. All available emoji (~1800) are rendered into the DOM in one pass, taking up to 2-3 seconds to complete on older machines. This pull request addresses this issue by using 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's CellMeasurer 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.

  1. react-virtualized's CSS needs to be loaded by the app. This is located in react-virtualized's package at react-virtualized/styles.css. We could inline these styles into css/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? Update: The stylesheet will not be needed, see my later comment.
  2. react-addons-shallow-compare needs to be installed as an app dependency.

@tommoor
Copy link
Owner

tommoor commented Feb 1, 2017

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.

@lochstar
Copy link

lochstar commented Feb 1, 2017

Great idea @ahutchings.

Just a note that react-addons-shallow-compare is deprecated. You can use PureComponent instead.

@ahutchings
Copy link
Contributor Author

ahutchings commented Feb 1, 2017

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.

@ahutchings
Copy link
Contributor Author

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.

@tleunen
Copy link

tleunen commented Feb 3, 2017

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 :)

@ahutchings
Copy link
Contributor Author

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.

Copy link
Owner

@tommoor tommoor left a comment

Choose a reason for hiding this comment

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

Overall these changes look great! I've pointed out a couple of quick improves in the code and I also noticed that the emojis and skin tone picker are not aligned correctly on the right with the search box…

image

@@ -0,0 +1,30 @@
export default function createEmojisFromStrategy(strategy) {
Copy link
Owner

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 💯

@@ -0,0 +1,71 @@
import {chunk, map, values} from 'lodash';
Copy link
Owner

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';

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -0,0 +1,163 @@
import React, {PropTypes, Component} from 'react';
import {AutoSizer, List} from 'react-virtualized';
import {findIndex, throttle} from 'lodash';
Copy link
Owner

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

import EmojiRow from './emoji-row';
import Modifiers from './modifiers';

const CATEGORY_HEADER_ROW_HEIGHT = 46;
Copy link
Owner

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

role="option"
key={emoji.unicode}
onClick={function(e) {
onChange(emoji);
Copy link
Owner

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 😄

Copy link
Contributor Author

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.

Copy link

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).

@tommoor
Copy link
Owner

tommoor commented Feb 11, 2017

It's funny how many problems I can spot in my own code from just 6 months ago… Thankfully you tackled many of them 😆

@ahutchings
Copy link
Contributor Author

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.
if (e.which === 13 || e.which === 32) {
onChange(emoji);
}
onSelect={() => {
Copy link

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.

Copy link
Contributor Author

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.

@tommoor tommoor merged commit 4a22b9c into tommoor:master Feb 12, 2017
@tommoor
Copy link
Owner

tommoor commented Feb 12, 2017

I'm working on putting together a 2.0.0 release with these changes and some other minor fixes, thanks for all your work! 🙌

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants