Q:

React Performance Issues in Firefox?

I'm experiencing some performance issues with a react application that I developed. These issues specifically (or most notably) occur with Firefox (both FF developer 77.0b7  and FF 76.0.1).
When using this application in Firefox, CPU usage gets extremely high, and my fans start spinning up to very high speeds. I get about 15-19fps in firefox according to the performance tools in FF. I get roughly 60fps in Chrome and Safari.
These issues occur when I begin typing into the input field, and get worse as the input gets longer (which makes sense)
The application is available here:
https://text-to-aura-generator.netlify.app/
Source code available here: https://github.com/paalwilliams/Text-to-Aura/tree/master/src
I'm almost certain that this is something I'm doing incorrectly, or that I've written the code inefficiently, but that isn't necessarily supported by the stark performance difference between browsers. Is chrome just that much better and handling react/constant rerenders?
I know that this is a broad question, but I honestly don't understand what is happening here, or necessarily how to troubleshoot it beyond the developer tools. Any input or thoughts would be greatly appreciated.

A:

The problem is your application is rendering too fast. In your particular case, there a few ways to improve that.
Every time you update the state, React needs to re-render your application, so updating the state within a loop is usually a bad idea.
Also, you are using useState 3 times, but only colors should be there, as App actually needs to re-render to reflect the changes there. The other two pieces of state (text and hex) are only being used to pass data from the handleChange to the callback inside useEffect.
You can restructure your code to:

Avoid updating the state within a loop.
Use a simple variable instead of state.
Use useCallback to define a function with that logic that is not re-created on each render, as that forces TextInput to re-render as well.
Throttle this callback using something like this:
import { useCallback, useEffect, useRef } from 'react';

export function useThrottledCallback<A extends any[]>(
  callback: (...args: A) => void,
  delay: number,
  deps?: readonly any[],
): (...args: A) => void {
  const timeoutRef = useRef<number>();
  const callbackRef = useRef(callback);
  const lastCalledRef = useRef(0);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setTimeout kicks in, it
  // will still call your old callback.
  //
  // If you add `callback` to useCallback's deps, it will also update, but it
  // might be called twice if the timeout had already been set.

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Clear timeout if the components is unmounted or the delay changes:
  useEffect(() => window.clearTimeout(timeoutRef.current), [delay]);

  return useCallback((...args: A) => {
    // Clear previous timer:
    window.clearTimeout(timeoutRef.current);

    function invoke() {
      callbackRef.current(...args);
      lastCalledRef.current = Date.now();
    }

    // Calculate elapsed time:
    const elapsed = Date.now() - lastCalledRef.current;

    if (elapsed >= delay) {
      // If already waited enough, call callback:
      invoke();
    } else {
      // Otherwise, we need to wait a bit more:
      timeoutRef.current = window.setTimeout(invoke, delay - elapsed);
    }
  }, deps);
}

If the reason to use useEffect is that you were not seeing the right values when updating colors, try using the version of setState that takes a callback rather then the new value, so instead of:
setColors([...colors, newColor]);

You would have:
setColors(prevColors => ([...prevColors , newColor]));

