Pull up a chair and warm your hands by the fire. Let me share with you three stories from my front-end performance adventures.
A bit of backstory
You can read an introduction to
react-beautiful-dndhere: Rethinking drag and drop
My first story: Moving naturally
When a user drops an item,
react-beautiful-dnd uses a physics based animation to give the impression that the dropping item has weight.
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
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
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
We started with a physics based
spring value that we liked and then reverse engineered an animation and curve to match it. It would be cool if somebody built a library that takes a
spring and returns a CSS animation with dynamic duration (css-spring looks promising). It would be even more interesting if these animation curves could be computed as part of a build step and not at run time
My second story: Hot functions
When a user starts a drag there is a lot of work
react-beautiful-dnd needs to do. It captures and processes a lot of information to build a virtual model of the things it cares about. It does this as the drag is starting so it knows that the information it has is completely up to date. Unfortunately this is the worst time to do any heavy work as we want drag and drop operations to feel as light as possible. I have previously done a lot of work to make the lift phase very performant, which you can read about in “Dragging React performance forward”.
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.
My high level thought was this:
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
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
- Page load is important. Even though
requestIdleCallbackwill 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
CSS has a very interesting property:
will-change. This property provides a hint to browsers that a particular CSS property will change in the future and optimise for that.
My third story: Being open
A bug was raised for
react-beautiful-dnd where it was found in particular setups there could be some jittering 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%.
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?