Highly inefficient invisible animations (CSS/Firefox/Chrome/React)
Tags: webdev, javascript, react, css
Published: 2021-01-16 16:06:46

---
title: Highly inefficient invisible animations (CSS/Firefox/Chrome/React)
published: true
description: Reducing the CPU load of Firefox and Chrome due to invisible CSS animations
tags: webdev, javascript, react, css
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/1iudk37z99ecin3okjzr.jpg
canonical_url: https://mortoray.com/2021/01/16/highly-inefficient-invisible-animations-css-firefox-chrome-react/
---

The cursor in my text editor was lagging. It’s quite unusual given my 8 cores machine with 32GB of RAM. While tracking down that issue, I discovered that [my escape game](https://edaqa.link/Carnival-Dev) was consuming 20-30% of the CPU while idling. That’s bad! It turns out it was invisible elements being rotated via CSS.

It’s a bit of a pain. This means we need to remove all those elements which fade-away, otherwise they pile up and create load. Here I’ll show you my solution using React — the top-layers of my game are in React, that’s why I used it. I’m not suggesting you use React to solve this problem.  But if you have animated HTML elements, get rid of them if they aren’t visible.

# The Problem

While loading scenes, I display an indicator in the top-right corner of the screen.

{% youtube 9oW3J-NYY5Y %}

This fades in when loading starts and fades out when loading is done. I wanted to avoid an abrupt transition. I handled this with CSS classes to hide and show the element. My React code looks like this:

```jsx
	<SVGElement 
		url={url}
		className={RB.class_name("load-marker", className, is_loading && 'loading')}
	/>
```

`SVGElement` is my component to load SVG files and display them inline. An `img` tag will perform the same way for this setup. The key is the `is_loading && ‘loading’` part of the `className` attribute. This adds the `loading` class name to the element while it’s loading. When finished loading, I remove the class name.

This is the CSS (SCSS):

```scss
.load-marker {
	&:not(.loading) {
		animation-name: fade-out;
		animation-fill-mode: forwards;
		animation-duration: 0.5s;
		animation-timing-function: ease-in-out;
	}
	&.loading {
		animation-fill-mode: forwards;
		animation-duration: 0.5s;
		animation-timing-function: ease-in-out;
		animation-name: fade-in;
	}
	@keyframes fade-out {
		from {
			opacity: 1;
			visibility: visible;
		}
		to {
			opacity: 0;
			visibility: collapse;
		}
	}
	@keyframes fade-in {
		from {
			opacity: 0;
			visibility: collapse;
		}
		to {
			opacity: 1;
			visibility: visible;
		}
	}
}
```

>I have an urge to digress into a rant about CSS's animation system!  I've written animation and layout systems before, and argh, this is acid thrown in my eyes. Indeed, [that system](https://fuseopen.com/) has a clear adding and removing animation support, making this whole setup trivial. But this is CSS, and, alas…

When an item loses the `.loading` class it will transition to a transparent state. The problem however came from some other CSS:

```scss
.loader {
	svg {
		animation: rotation 6s infinite linear;
		overflow: visible;
		position: absolute;
		top: 20px;
		right: 20px;
		width: 70px;
		height: 70px;
	}
	@keyframes rotation {
		from {
			transform: rotate(0deg);
		}
		to {
			transform: rotate(360deg);
		}
	}
}
```

That `infinite` bit is the problem. It’s irrelevant that we’ve faded the opacity to 0, the animation is still running! Firefox still does a style and layout update, each frame. Why it ends up consuming so much CPU, I have no idea.  Chrome also consumed CPU, but only around 10%.  Note, 10% is still ridiculous for a static screen.

I could also “solve” the problem by not spinning the item unless something is loading. This creates a rough transition where the icon abruptly stops rotating while fading away. Not good.

# The Solution

I have two animated indicators, the loader and a disconnected icon, for when you lose the WebSocket connection to the server. I abstracted a common base component to handle them the same. This is how I use it, for the loader:

```jsx
export function Loader({ is_loading }) {
	return <HideLoader
		url={theme.marker_loading}
		is_loading={is_loading}
		className="loader"
	/>
}
```

This is the implementation:

```jsx
function HideLoaderImpl({ is_loading, url, className }) {
	const [ timer_id, set_timer_id ] = React.useState(0)
	
	React.useEffect(() => {
		if( !is_loading && !timer_id ) {
			const css_duration = 1000
			const new_timer_id = setTimeout( () => set_timer_id(0), css_duration )
			set_timer_id(new_timer_id)
		}
	}, [is_loading]) // only trigger on an is_loading change
	
	const visible = is_loading || timer_id
	if(!visible) {
		return null
	}
	
	return (
		<SVGElement 
			url={url}
			className={RB.class_name("load-marker", className, is_loading && 'loading')}
		/>
	)
}

const HideLoader = React.memo(HideLoaderImpl)
```

At first glance, it’s not obvious how this achieves a delayed removal of the element. The HTML generation is clear, when `visible` is false, then display nothing. When true, display the element as before, with the same logic for setting the `loading` class name.

If `is_loading` is true, then `visible` will be true. This is the simple case. But there is the other true condition when we have a `timer_id`. 

The `setTimeout` callback does nothing but clear the `timer_id` when it’s done. At first I suspected I’d have to track another variable, setting at the start and end of the timeout. It turns out that all I need to know is whether there is a timeout at all. So long as I have a timer, I know that I shouldn’t remove the element.

The condition list to [`React.useEffect`](https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect) is important here. I provide only `is_loading` — I only wish for the effect to run if the value of `is_loading` has changed. Some style guides will insist that I include `timer_id` (and `set_timer_id`) as well in the list. That approach defines the second argument to `useEffect` as a dependency list, but this is incorrect. It’s actually a list of values, which if changed, will trigger the effect to run again.  The React documents are clear about this. Yet also say it’s a dependency list, and recommend a lint plugin that would complain about my code. That recommendation makes sense for `useCallback` and `useMemo`, but not for `useEffect`.

Adding `timer_id` to the list would be wrong. When the timer finishes, it sets the `timer_id` to 0. That change would cause the effect to trigger again. This is a case where we do “depend” on the `timer_id` value, but we shouldn’t re-execute when it changes, as that would end up creating create a new timer.

In any case, this simple code now does what I want. It defers the DOM removal of the element until after the end of the animation. Well, it defers it one second, which is long enough to cover the 0.5s CSS animation. It’s complicated to keep these times in sync — _more fist shaking at the CSS animation system!_

>If you’ve got an eye for defects, there is one there. The loader icon can be removed too early: when `is_loading` becomes true, then false, then within one second becomes true and false again. I don’t create a new timer if one already exists, so the deferral time will still be from the first timer. In practice, this will not likely happen, and the impact is minimal. The fix is to cancel an existing timeout and always create a new one.

# My lagging cursor

I never got an obvious answer why my cursor was lagging. There were all sorts of applications, idle applications, consuming 5-10% CPU.  It’s perhaps a real cost of high-level languages. More on that another day. I still hope that future apps will strive for less energy use.

For now, remove all those invisible animated HTML elements.