godblessstrawberry
godblessstrawberry

Reputation: 5058

React clock component with interval updates is very slow

I'm new to React, trying to create analog watch as exercise and all was good until I set update interval to 1000 / 30 (to have ~30fps). Its extremely slow - CPU is always 100, memory leaks, event listeners number easily grows to 30k. Devtools stops it in a while to prevent out-of-memory-crash. What am I doing wrong here? Should I use requestAnimationFrame() instead of interval?

enter image description here

Tried to make snippet below via instruction, but no luck - working example is only on stackblitz https://stackblitz.com/edit/react-roszta?file=index.js

const {useState} = React;

function DigitalDisplay(props) {
    const time = new Date(props.time).toLocaleString();
    const style = {
        width: '200px',
        height: '30px',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        background: '#444',
        boxShadow: '0 0 0 3px cyan',
        borderRadius: '3px',
    }
    return (<div style={style}>{time}</div>);
}

function AnalogDisplay(props) {
    const style = {
        width: '200px',
        height: '200px',
        position: 'relative',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        background: '#444',
        borderRadius: '50%',
        transform: 'rotate(-90deg)',
        boxShadow: '0 0 0 3px cyan'
    }
    return (<div style={style}>
        <Arrows time={props.time} />
    </div>);
}

function Arrows(props) {
    const style = {
        display: 'block',
        background: 'white',
        height: '0',
        transformOrigin: '0 0',
        boxShadow: 'cyan 0px 0px 0px 2px, rgba(255,255,255,0.8) 0px 0px 7px 1px',
        borderRadius: '3px',
        position: 'absolute',
        left: '50%',
        top: '50%',
    }

    const styleHours = {
        ...style,
        width: '20%',
        transform: `rotate(${new Date(props.time).getHours() * 360 / 12}deg)`
    };

    const styleMinutes = {
        ...style,
        width: '30%',
        transform: `rotate(${new Date(props.time).getMinutes() * 360 / 60}deg)`
    };

    const styleSeconds = {
        ...style,
        width: '45%',
        transform: `rotate(${new Date(props.time).getSeconds() * 360 / 60}deg)`
    };

    const styleMilliseconds = {
        ...style,
        width: '2000%',
        boxShadow: '0px 0px 1px 1px yellow',
        opacity: 0.8,
        transform: `rotate(${new Date(props.time).getMilliseconds() * 360 / 1000}deg)`
    };

    return (<React.Fragment>
        <div className="arrow-milliseconds" style={styleMilliseconds}></div>
        <div className="arrow-seconds" style={styleSeconds}></div>
        <div className="arrow-minutes" style={styleMinutes}></div>
        <div className="arrow-hours" style={styleHours}></div>
    </React.Fragment>)
}

function Clock() {
    const [time, setTime] = useState(new Date().getTime());
    setInterval(() => {
        setTime(new Date().getTime())
    }, 1000 / 12);
    return (<React.Fragment>
        <AnalogDisplay time={time} />
        <br />
        <DigitalDisplay time={time} />
    </React.Fragment>);
}

ReactDOM.render(
    <Clock />,
    document.getElementById('root'),
)
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background: #444;
  color: cyan;
  width: 100vw;
  height: 100vh;
  padding: 0;
  justify-content: center;
  align-items: center;
  display: flex;
  overflow: hidden;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

Upvotes: 1

Views: 537

Answers (2)

corvus
corvus

Reputation: 513

That's what I've got so far

This image is your current code as it is

With AnalogDisplay and DigitalDisplay

I removed AnalogDisplay and DigitalDisplay, The JS event listeners had dropped drastically, meaning that it is not a problem with react itself but with the clock displays

enter image description here

Here I changed your fps to a frame every 5 seconds As you can see the CPU usage had dropped back to normal

Render every 5 seconds

The last one I changed your Clock component to a stateful component, and rather than using useState I just edited the component's state Frame rate is 30fps and CPU usage is 40% at its worst

enter image description here

At first I thought that you've got some poor CPU, but mine is i7-7700HQ, so CPU usage should never hit such numbers. It appears that React's useState's setter function does use a lot more CPU to render its component than the normal Stateful component.

Upvotes: 1

Thomas
Thomas

Reputation: 12637

You are creating quite a few objects, and that every 33ms. What you see there is GC in action. Have you considered letting CSS do the animation?

(function(clock, now) {
  // setting the clock once.
  clock.style.setProperty(
    "--hours",
    now.getHours() + now.getMinutes() / 60
  );
  clock.style.setProperty(
    "--minutes",
    now.getMinutes() + now.getSeconds() / 60
  );
  clock.style.setProperty(
    "--seconds",
    now.getSeconds() + now.getMilliseconds() / 1000
  );
})(document.getElementById("clock"), new Date());
#clock {
  position: relative;
  width: 150px;
  height: 150px;
  border: 1px solid currentColor;
  border-radius: 50%;
  transform: rotate(180deg);
}

.hours,
.minutes,
.seconds {
  position: absolute;
  top: 50%;
  left: 50%;
  display: block;
  width: 0;
  border: 1px solid currentColor;
  transform-origin: 50% 0%;
  animation-name: rotate;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.hours {
  height: 20%;
  animation-duration: 43200s;
  animation-delay: calc(var(--hours) * -3600s);
}

.minutes {
  height: 40%;
  animation-duration: 3600s;
  border-color: #666;
  animation-delay: calc(var(--minutes) * -60s);
}

.seconds {
  height: 45%;
  animation-duration: 60s;
  border-color: rgba(black, .25);
  animation-delay: calc(var(--seconds) * -1000ms);
  animation-timing-function: steps(60);
}

@keyframes rotate {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(1turn);
  }
}
<div id="clock">
  <div class="hours"></div>
  <div class="minutes"></div>
  <div class="seconds"></div>
</div>

Upvotes: 0

Related Questions