Josh Goldberg
Grid of various skin tone emojis with red vertical pipe line characters between them.

Speeding Up Centered Part 5: Emoji Processing

Sep 20, 202315 minute read

Finding and eliminating thousands of calls to recreate a giant regular expression in a popular open source emoji plugin.

This post is the fifth in a series of five performance improvements I made to the Centered app. See 81 <iframe> Embeds for more context.

  1. 81 <iframe> Embeds
  2. Hidden Embedded Images
  3. Barrel Exports
  4. Unused Code Bloat
  5. 👉 Emoji Processing

Issue 5: Emoji Processing

The previous few performance improvements I’d made for Centered were mostly focused on reducing the amount of data loaded on each page. But runtime performance -especially on page load- is important too. And while I’d improved the load performance of one page back in the 81 <iframe> Embeds work, I’d noticed an odd slowdown upon visiting any page. It was as if the page was performing multiple seconds of busywork before it could run any other scripts.

This last performance improvement is my favorite of the bunch. I ended up finding a root cause embedded in a cross-section of interesting open source libraries. And, best of all, I was able to fix it for all users of the libraries. ☺️

1. Identification

I popped open the Chrome dev tools Performance tab and ran Start profiling and reload page on a local production build of Centered. The resultant flamegraph showed a lovely breakdown of how long function call took in code and who it called.

Taking more than a second to run scripts on page startup is not good. It was also interesting that it seemed to be calling the same toShort and replaceAll repeatedly. Why was the code spending so much time in those functions?

2. Investigation

I drilled down to see the long task’s contents coming from a package called @draft-js-plugins/emoji. Draft.js is a popular text editor component for React. @draft-js-plugins/emoji is a plugin that, as the name suggests, adds rich emoji support.

I opened that file in the Chrome dev tools Sources panel to see the line-by-line view of which lines in source code were taking up time:

Multiple seconds spent in two functions that seem to be processing/replacing text in strings? Exciting! Juicy bits of slow code like that are gold for performance investigations such as this one. They almost always end up with some straightforward optimization to avoid repeat work.

Sharing the Enthusiasm

I’d recently seen the excellent Speeding up the JavaScript ecosystem series of posts by Marvin Hagemeister. Although Draft.js emoji plugins aren’t as widely used as the other findings Marvin had blogged about, I still thought Marvin might find this interesting. I put together an isolated reproduction and sent him an email describing the performance findings thus far.

Marvin replied less than a day later expressing interest in the issue. I suggested we pair program on it and we hopped on a call within the hour. Marvin has a great deal of experience in performance investigations, so I was thrilled to get to learn from him.

3. Implementation

You can see Marvin’s summary of the investigation in Speeding up the JavaScript ecosystem - draft-js emoji plugin. Marvin does a great job of explaining the path of discovery. In short:

  1. ns.escapeRegExp was being called thousands of times
  2. Each of those calls always received the same argument from ns.replaceAll’s find parameter
  3. The culprit calling ns.replaceAll repeatedly was a parent ns.toShort function:
    ns.toShort = function (str) {
    	var find = ns.unicodeCharRegex();
    	str = ns.replaceAll(str, find);
    	return str;
    };
  4. The find variable’s regular expression was huge -dozens of kB- and being recreated each time
  5. Caching the result of creating that regular expression saved over 90% of the time bloat (!)

We sent the results to the joypixels/emoji-toolkit repository:

I attached a Performance Profiles zip folder to the emoji-toolkit issue showing the before & after performance profiles. The changes are glorious:

Marvin’s post also mentions further improvements that could be done to shrink that time even more. For now, this improvement was quite nice!

4. Confirmation

Open source pull requests can take a while. I used patch-package to apply the changes locally in the meantime, then re-ran the production build and measured it locally with Lighthouse.

The results were fantastic: an increase from 51 to 65% in the overall performance score!

I’m happy to say that Centered shipped the performance improvements in April of 2023 and emoji-toolkit 8.0.0 released in August of 2023 with the performance fix. Cheers for a faster web! 🥳

In Conclusion

I had a great time running these performance investigations. Over the course of the series, I got to uncover some hilariously huge embedded media, dive into Next.js barrel exports bundling, apply custom ESLint rules as codemods, and flex using unused code detection with Knip. And -best of all- I got to work with Marvin Hagemeister on improving a slowdown in a popular open source library.

More work could of course be done to get that 65 score closer to 100, but that’s a matter for someone else to take on. I’m satisfied with the sum of improvements to Centered. Getting to work on low-size, high-impact performance improvements is always a treat when I have the time for it.

I hope you read these posts and get excited to run your own performance investigations. If you do, remember:

Thanks for reading! 💖

  1. 81 <iframe> Embeds
  2. Hidden Embedded Images
  3. Barrel Exports
  4. Unused Code Bloat
  5. 👉 Emoji Processing