Grabbing the flame 🔥

Alex Reardon
Nov 5, 2018 · 6 min read

Pull up a chair and warm your hands by the fire. Let me share with you three stories from my front-end performance adventures.

Photo by Joshua Newton on Unsplash

A bit of backstory

You can read an introduction to react-beautiful-dnd here: Rethinking drag and drop

My first story: Moving naturally

Physics based drop animation

react-beautiful-dnd uses react-motion to achieve this. react-motion bases an animation on a spring rather than on traditional animation properties such as curve and duration. Using a spring helps give the drop a weighted feeling as we can define an animation using physical properties such as stiffness and damping.

I approached a colleague of mine at Atlassian, Jacob Miller, with this proposal:

“Can we create the same or similar animation using only CSS? Ideally, if it could look the same that would be great, but I would be happy if we could find a CSS animation that was almost the same.”

If we could use CSS for the drop animation then we could remove react-motion to lower our bundle size, as well as moving the workload for the drop animation off the main thread of the CPU to the GPU.

Without looking at the code itself, Jacob plotted the percentage of the distance travelled of dropping item against the percentage of the time taken for the animation to complete. What he found was that regardless of where the user dropped, it produced a consistent animation curve. The only thing that changed was the duration.

With this information, I was able to achieve the exact same drop animation that react-motion provided, using only a CSS animation with a dynamic duration

Result curve [cubic-bezier(.2, 1, .1, 1), with duration 0.33s — 0.55s]

This change resulted a 66% performance improvement when dropping in big lists on less powerful devices. I also found a 25% performance improvement when dragging, long before a drop animation has started. This is because we no longer need to pay any cost for passing values through react-motion

Our use case only involved a one time animation with a consistent spring value for all drops. This made it a great candidate for a CSS animation. More complex animations would still require an animation library.

Adventure for another: Spring to CSS

My second story: Hot functions

My thinking was,

“Could I make a code path run even faster to improve lift times?”

I decided to try something I had been thinking about for a while: optimising functions.

When a JavaScript engine executes a function a number of times, it can decide to optimise the function. This is some additional work by the JavaScript engine which enables the function to be executed faster when it is run again in the future.

Taken from the great talk “JavaScript Engines: The Good Parts

My high level thought was this:

“Could I do some redundant work ahead of time to give a clue to the JavaScript engine to optimise these functions, so that when it comes time to use them they are already optimised?”

My approach was to use requestIdleCallback to schedule some low priority future work for each draggable item. In this idle callback I would call a function that I wanted to be optimised in a redundant loop with the following guards:

  • Only call the function a maximum of 10 times
  • Only let the loop run for a maximum of 1ms
  • Only run while the idle callback has remaining time, by using deadline.timeRemaining

I would also cancel the idle callback if a drag starts as a warm-up would not be needed any more.

This little experiment worked. I observed consistent cross-browser improvements in lift performance of about 25%.

What might be more surprising is that I did not ship this optimisation. Whenever we write code we are making trade-offs. In this case, on balance I thought it better not to ship this optimisation for now. Here are some of the reasons I did not ship the optimisation:

  • The optimisation is extremely speculative that might stop working tomorrow if engines change how and when they optimise functions. I would need to be regularly testing it to see if it was still working
  • No idea if the numbers I chose (call count, runtime) would yield consistent results across devices
  • Lifting is already very optimised and occurs in under 30 ms for a 500 item list. This optimisation saves about 10ms in a 500 item list at best. In order to get this 10ms boost, we cost the user 1ms per item, which is 500ms of CPU time. That is a 50:1 cost to benefit ratio. I felt like this was a glory gain for react-beautiful-dnd
  • Page load is important. Even though requestIdleCallback will only do work when the main thread is not busy, I do not want to mess around with page startup performance
  • The optimisation adds more code and complexity to the code base

Adventure for another: will-change

I think it would be valuable to also have language semantics for this within JavaScript. We could hint that particular functions will be hot so that a JavaScript engine could optimise them without needing to be run multiple times.

This could be nice

My third story: Being open

Jitter when auto scrolling

After digging into the bug I discovered two functions were solving similar problems, but looking at the problem from a different perspective. There was a minor mismatch in how they were viewing things which is what caused the bug.

I also noticed that both of these functions were doing some serious number crunching to solve this problem on every update. By thinking about the problem a bit differently I was able to pull this shared calculation out into its own function and cache the result.

Fixing this bug had an indirection result of improving our performance by about 5%.

Auto scroll bug has been fixed with a 5% application performance improvement

To me this was really fascinating. A bug came in, and in fixing the bug I improved performance. My personal experience is that it is easy to discount bugs, especially those we consider ‘paper cuts’ and move onto ‘more important things’. What if instead we saw bugs as an opportunity to question our abstractions rather than a chore to be avoided?

*Stares into the flame*

Thanks for hearing me out. Now, what have you been up to?