Problem

You have a UI piece that is performing some action on every frame — an animation that is following continuous user input like a knob or a “range input” would do. All was dandy as long as you use a simple component or you managed to optimise rendering of it, but it got more complex or you just found out that on some older device that you have to support it doesn’t work as good, as on your superb Galaxy/Pixel/iPhone, part of you dev bling.

On web, rendering hook requestAnimationFrame is capped on 60fps and (for now) never goes higher, for some mobiles it goes under that, but favouring steadiness of frame rate above average speed, even on newer devices that theoretically would allow you to go further (120Hz mobile screens as a standard may here be soon!)

When you are way below 30fps, like 1fps 2fps, you are having more serious problem and more specific too, so let me state first that this is not about that kind of scenario. What I’ll help you with here is about having a 60fps target (as you should have) and actual performance between 20–60fps.

Analysis — What your problem is really about?

When you try to follow user interaction what you’ll do is update app UI whenever you get an information about that interaction, so user feels like it has a direct and often even physical influence over that part of UI. Knob example fits here quite well as it has a real world counter part and that is conveying an expectation how this should operate. But in real world, you turning a knob, have an actual, physical control over that object whereas when it is UI you have to update your UI to match that state.

Update of UI is never instantaneous, even if it is pretty fast. Reacting to user input providing steady 60fps means an update, a single frame every 16.(6)ms. In browsers that means less time for calculations as render (layout/composition/paint) is handled by a browser. On mobile, if you don’t draw pixels directly onto screen, you’ll have an overhead as well. Lag is where you (and whatever aids you in render) take more than that 16.(6)ms.

Where on high-end devices you should never have a lagging UI and by that I mean you should never produce a code that will cause that, you always can hold performance on older/weaker devices to lower standard. That still doesn’t mean unresponsive UI. So what to do when you just cannot reach target framerate?

(🌍Web) target=60fps: use “cheaper” properties

Rendering in browser consists of few steps:

layout

(then) paint

(then) composition

Then, there are some properties that in some cases overlap with their function, yet they trigger different (smaller) set of rendering steps. Having this problem in mind, most common replacement will probably be position to transform.translate .

As you can read provided this list of causes, moving an element inside a viewport using transform should be more appropriate as it triggers only composition step and therefore leaves more time for your code to execute. If you need to move a node on the screen, but not to change it’s size — use transform.

(🌍Web) target=60fps: Use paint layers

So previous trick will mainly allow you to sometimes skip layout phase. This one will focus on paint phase. csstriggers.com may still help, but this time it is about more active approach.

Paint is slightly different phase it terms of how work is organised. Whereas layout is calculated as a whole, painting is done on layers — every layer is made up separately. Therefore, whenever there’s a paint trigger it is for that layer where trigger happened. We can leverage that.

Check an article about stacking context on MDN to see what causes a layer to constructed. These will be:

position =relative + z-index

=relative + position =absolute + z-index

=absolute + position =fixed

=fixed being flex root child + z-index

child + opacity other than 1

other than 1 mix-blend-mode

transform

filter

perspective

clip-path

mask , mask-image , mask-border

, , -webkit-overflow-scrolling =“touch”

=“touch” will-change “specifying any property that would create a stacking context on non-initial value”

“specifying any property that would create a stacking context on non-initial value” and one that is just for sole purpose o creating new layer: isolation=isolate, however is not yet reliably supported yet

Knowing that, we can make sure that an UI element that is following user interaction is not on the same layer as the rest of the app. Any paint triggering property upon change will trigger paint only for that single layer.

Even better when the rest of the UI is on a single separate layer. That means that also composition (merging layers) can be done faster — due to merging only two layers instead of amount that would surround animated element. That can be most easily achieved with still pretty new React Portals. You can put a dedicated node just next to app root node (one you mount your app onto) and make it so both of them are made into layers.

(🌍📱All) target=60fps: Defer acknowledgement of new state

This is for a subset of mentioned cases where a lag is not introduced not because of the animated element itself, but due to the element being a controlled component driven with a value(s) that drive something else, something “heavy”.

You can throttle submitting the value making so the animated element itself is in a state of limbo between having state and being controlled — having a state while updates are throttled, then at once submitting update and dropping it’s state, then repeat. This might still cause junk (breaks in responsiveness), yet while it is bad, it will be overall better.

(📱Native) target=60fps: Use Animated and (if possible) useNativeDriver

Although React Native allows you to start fast, that initial ease (as it often is) will take it’s vengeance. This is not something that React Native was built for neither is good at.

When you have a scenario where you can use event driven approach with Animated , use it. Although the service — it’s necessity — serves as a case against React Native, it is quite good solution when faced the problem. It has quite good interface, too bad that documentation could be a lot better and sometimes is confusing (which also indicates that this is a zone where React Native “ends”). When you cannot use Animated you’re left with next approach.

(🌍📱All) target=60fps: Compensate and predict

When all other means fail, the element is still lagging behind user input, take a leap into the future ✨

You could also apply this technique to elements that perform well, but usually this would make element more complex with little to no visible performance, so it would be waste of a workforce.

So, you missed no news — time machine is still unavailable, but I’ll show you how, using a prediction you can better UX.

Below is a representation of frames, frame hook and lagging code. For simplicity, let’s pretend that browser/native work on render is part of our code. This is what would normally happen: