Slack is transitioning its web client to React. When Slack was first built, our frontend consisted of established technologies like jQuery and Handlebars. Since then, the community has developed better ways to create scalable, data-driven interfaces. jQuery’s “render and modify” approach is straightforward, but it’s prone to falling out of sync with the underlying model. In contrast, React’s “render and re-render” pattern makes consistency the default. Slack is evolving alongside the industry to improve performance and reliability.
We determined that the best way to introduce React would be to rebuild an existing product feature — that way, we could compare the development process and end result to a known quantity. We wanted a component that was interactive, self-contained, and demanding enough to prove our assumption that React could improve performance. It didn’t take long to find a perfect candidate — the highly used and surprisingly complex Emoji Picker.
Virtual DOM with actual benefits
This post assumes some knowledge of React. If you’re unfamiliar with it, I’d suggest browsing the official docs. Briefly, React is a JavaScript library that makes it easy to write declarative, data-driven user interfaces. The API is trim, consisting primarily of a Component class that includes a handful of lifecycle methods. Components don’t generate markup on their own. Instead, they render into a DOM-like tree called the Virtual DOM. React can compare two Virtual DOM trees to determine the fewest actions required to transform the first tree into the second. For instance, you could tell React to re-render the entire view with new model data, and it might determine that it only needs to update the text of a few nodes. React is like having an army of gnomes making bespoke DOM updates on your behalf.
React excels in consolidating all the ways a component can change into a single template. For example, consider how you’d use vanilla JavaScript to update Slack’s channel sidebar when a channel becomes unread:
- Identify the modified channel’s ID and unread state
- Query the DOM for the channel element using the channel ID
- Toggle the unread class on the channel element
The process is straightforward, but you’d have to write additional handlers to support other channel events like “create”, “join”, “leave”, and “rename”. In contrast, React accommodates all five scenarios with:
- Re-render the channel sidebar with new model data
Instead of handwriting each DOM update, we get to re-render the entire component and let React figure out how to do it efficiently. This approach streamlines development by trading specialized code paths for generic, one-size-fits-all templates.
Picking the Picker
Emoji are an integral part of Slack’s UI and the Emoji Picker is an ideal React component. It’s dynamic, discrete, and requires only a few inputs — a list of emoji, preferred skin tone, and the user’s historical emoji usage. And coincidentally, the current Emoji Picker was in need of a performance tune-up due to its unfortunate strategy of rendering every emoji regardless of whether it was in view. Searching involved toggling the visibility of every node that did or didn’t match the user’s search terms. It was performance death by a thousand cuts. New Slack teams start with 1,374 default emoji and it increases as you add customs (as of this writing, Slack’s team has 3,126 total emoji, but some teams have even more!) Rebuilding the Emoji Picker would give us the opportunity to impact everyday Slack usage in a meaningful way.
We chose to develop in Storybook, a self-proclaimed “UI development environment you’ll ❤️ to use”. It doesn’t replace your style guide, but it does make developing, testing, and reviewing code more pleasant. Storybook allows you to define variations of your component by specifying different sets of properties. For the Emoji Picker, we added a variant for skin tone preference and multiple variants for search. Anyone at Slack — developer or otherwise — can open Storybook and view the enumerated states.
Component layout
The React Emoji Picker is built from a stateful root component with many stateless children. We adhered to the convention of exporting a single component per file. The general structure is as follows:
Header
- Category tabs: lists emoji categories with “jump to” links
- Search bar: filters Emoji List contents by emoji name/alias
Body
- Sticky header: displays the name of the active category
- Emoji List: virtual list of emoji in all categories
Footer
- Emoji preview: large view of the currently selected emoji
- Skin tone picker: displays the active skin tone with the option to choose a new one
- Handy reactions (optional): subset of emoji commonly used to react to messages
There are 2 primary methods for writing stateless components in React: the PureComponent class and functions. Functions are simpler, but they render on every reconciliation which can hurt performance. The React team has plans to optimize functions, but for now it seems best to avoid them. We chose instead to use PureComponent, which includes a predefined shouldComponentUpdate method that prevents updates when passed identical props.
Since React is just a view layer, incorporating it into an established application can be more straightforward than integrating a prescriptive framework. It was important for us not to compromise the new Emoji Picker’s encapsulated design in order to accommodate patterns that exist in Slack — we wanted the component to look like it had been plucked from an end-to-end React app. In order to keep the picker pure, we created a lightweight adapter in our existing module system. The adapter mounts the component, extracts model data, and listens to external signals. This pattern allows us to incrementally port the codebase while we develop new features.
New development workflow
While developing with React was a joy, integrating it into our pre-existing development workflow was not — at least not at first. At the time, Slack’s frontend build pipeline had been developed in-house and had no notion of imports, dependencies, or complex transformations like transpilation. We were, however, determined to take full advantage of JSX syntax as well as the benefits of writing ES2015+ JavaScript. In lieu of a modern build pipeline, we started out by building the Emoji Picker assets locally with Babel and webpack.
We expected that checking in locally compiled code would be painful, but we underestimated just how exasperating the ensuing merge conflicts and dependency management would be. As a result, we’re working on integrating webpack into our development and staging environments, with the goal of seamlessly replacing existing workflows. To accommodate this, we:
- Developed a service around webpack-dev-server to automatically compile and serve assets on our dev servers when watched files or their dependencies change (including webpack’s own config)
- Added support for loading webpack assets in unit tests (making it possible to write tests for our React components)
- Reworked our production build process to push webpack assets to our CDN
Rebuilding the Emoji Picker turned out to be exactly the impetus we needed to rethink our build pipeline to bundle and transpile assets in a more robust and scalable way.
Performance results
We deployed the new component to a small percentage of teams and observed the results. We measured render speeds across 5 common ways users interact with the Emoji Picker. The React implementation was noticeably faster for most actions. Listed below are the differences in render times for a typically sized team:
- First mount: -270ms (85% decrease)
- Second mount: -158ms (91.3% decrease)
- Search (many results): +27ms (259% increase)
- Search (one result): -25ms (53.2% decrease)
- Reset search: -68ms (70.1% decrease)
The biggest improvement came during “First mount” which dropped 270ms falling from 318ms to 48ms, an 85% decrease. This is largely attributed to react-virtualized — a virtual list library — which reduced the number of rendered emoji. The React Emoji Picker renders 85% fewer DOM nodes in the default view.
Perhaps the most surprising change came during “Search (many results)” which gained 27ms increasing from 17ms to 44ms. The legacy picker visually hid emoji that failed to match the search query which means it’s relatively fast when almost everything matches. This downside of this approach is evidenced by the “Search (one result)” and the “Reset search” scenarios which force the legacy picker to perform work on a larger set of emoji.
Next steps
Rebuilding the Emoji Picker in React resulted in faster rendering and simplified code that’s easier to maintain. We’re extending these gains to the rest of our codebase by transitioning fully to React. We have a lot of work left to do, and we’re excited for the positive impact the transition will have on the everyday experience of our customers. At the same time, we’re building in-house expertise with React so we can help push the platform forward.
Want to help Slack solve tough problems and join our growing team? Check out all our engineering jobs and apply today.
Apply now