setTimeout runs its callback codes much slower than without it

Viewed 191

We are struggling with a strange issue in using setTimeout function in Javascript at the React.
As using codes inside the setTimeout, it runs much slower than running the codes without the setTimeout!

As a comparison, the performance is resulted in:

Using setTimeout: 1391 ms
without using setTimeout: 15 ms

In API callback (eg. axios), the setTimeout version happens as well!


A simple sample code is shown in the following:
codesandbox

Can anyone explain what is happening?

import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
  const [value, setValue] = useState("");
  const [time, setTime] = useState("");

  const startSample = () => {
    const startTimeStamp = new Date().valueOf();
    for (let i = 0; i < 5000; i++) {
      setValue(`test-${i}`);
      // console.log(`test-${i}`);
    }
    const endTimeStamp = new Date().valueOf();
    setTime(`${endTimeStamp - startTimeStamp}ms`);
  };

  const handleClick1 = () => {
    startSample();
  };

  const handleClick2 = () => {
    setTimeout(() => {
      startSample();
    });
  };

  return (
    <div style={{ textAlign: "left" }}>
      <p>{value || "Please push that button!"}</p>
      <div>
        <button id="startBtn" onClick={handleClick1}>
          Start Normal
        </button>
        <button id="startBtn1" onClick={handleClick2}>
          Start With setTimeout
        </button>
      </div>
      <p>Result: {time}</p>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
2 Answers

React batches renders that are queued up within an event callback, so all of your calls to setValue in the click handler result in a single render once your handler finishes.

However, I do not believe React batches renders from within a setTimeout call. So it is rendering after each call to setValue in your setTimeout handler.

See this issue: https://github.com/facebook/react/issues/14259

You should be able to make the setTimeout version faster by writing it like so:

const handleClick2 = () => {
  setTimeout(() => ReactDOM.unstable_batchedUpdates(() => startSample()));
}

If you have a bunch of async ajax responses coming back and want to apply them after they all arrive, then you might have code like this:

const [a, setA] = useState();
const [b, setB] = useState();
const [c, setC] = useState();
const [d, setD] = useState();

useEffect(() => {
  (async () => {
    const a = await fetchA();
    const b = await fetchB();
    const c = await fetchC();
    const d = await fetchD();

    // wrap all of the state updates in batchUpdates
    // so that we only get one render instead of 4
    ReactDOM.unstable_batchUpdates(() => {
      setA(a);
      setB(b);
      setC(c);
      setD(d);
    });
  })()
}), []);

React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like a setTimeout().

Consider the following

  const startSample = () => {
    const startTimeStamp = new Date().valueOf();
    const arr = []
    for (let i = 0; i < 5000; i++) {
      arr.push(i)
      // console.log(`test-${i}`);
    }
    setValue(arr)
    const endTimeStamp = new Date().valueOf();
    setTime(`${endTimeStamp - startTimeStamp}ms`);
  };

Now both times are about the same right? As pointed out by Brandon React doesn't seem to wait all udpates finishes before triggering a new render, resulting in five thousand renders instead of just one. So use an accumulator to perform the iterations and set the state just once

Related