Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JeremyIglehart/8f495968c61d547587f122e26a265987 to your computer and use it in GitHub Desktop.
Save JeremyIglehart/8f495968c61d547587f122e26a265987 to your computer and use it in GitHub Desktop.
React Countdown Component Performance Study

Hello Fellow React Peoples,

I finally finished my basic <Countdown /> componant. You use it like this:

<CountdownTimer start={someJSDate} end={someOtherJSDate} />

Here's the code for the component:

No Throttle:

import React, { Component } from 'react'

import moment from 'moment'

class CountdownTimer extends Component {
  constructor(props) {
    super(props)

    this.state = {
      startTime: null,
      endTime: null,
      loadTime: null,
      delta: null,
      difference: null,
      animationFrame: null
    }
  }
  componentWillMount() {
    this.setState({
      startTime: this.props.start,
      endTime: this.props.end,
      loadTime: new Date(),
    })
  }
  componentDidMount() {
    this.tick()
  }
  tick() {
      this.setState({
        delta: new Date(+new Date() - this.state.loadTime),
        animationFrame: requestAnimationFrame(() => this.tick())
      })
  }

  render() {
    let {
      startTime,
      endTime,
      delta
    } = this.state
    return (
      <div>
        {
          moment(moment(endTime).diff(startTime - delta)).format('d [Days], h [Hours], m [Minutes], s [Seconds and] SSS [Miliseconds]')
        }
      </div>
    )
  }
}

export default CountdownTimer

Excellent, now that I have it basically working - let's check out the CPU profile of how this runs in Chrome:

No Throttle CPU Performance Screenshot

Hm... That seamed heavy... So I tried to optimize it.

Delta Math Throttle:

import React, { Component } from 'react'

import moment from 'moment'

class CountdownTimer extends Component {
  constructor(props) {
    super(props)

    this.state = {
      startTime: null,
      endTime: null,
      loadTime: null,
      delta: null,
      throttleFPS: null,
      throttleInterval: null,
      throttleTime: null,
      throttleDelta: null,
      difference: null,
      animationFrame: null
    }
  }
  componentWillMount() {
    this.setState({
      startTime: this.props.start,
      endTime: this.props.end,
      loadTime: new Date(),
      throttleFPS: 30,
      throttleTime: performance.now()
    })
  }
  componentDidMount() {
    // Need to wait for FPS to be set in componentWillMount() to set throttleInterval
    this.setState({
      throttleInterval: 1000/this.state.throttleFPS
    })
    this.tick(this.state.throttleTime)
  }
  tick(throttleTick) {
    let {
      throttleDelta,
      throttleTime,
      throttleInterval
    } = this.state

    throttleDelta = throttleTick - throttleTime

    if (throttleDelta > throttleInterval) {
      this.setState({
        throttleTime: throttleTick - (throttleTime % throttleInterval),
        delta: new Date(+new Date() - this.state.loadTime),
      })
    }

    this.setState({
      animationFrame: requestAnimationFrame(throttleTick => this.tick(throttleTick))
    })
  }
  render() {
    let {
      startTime,
      endTime,
      delta
    } = this.state
    return (
      <div>
        {
          moment(moment(endTime).diff(startTime - delta)).format('d [Days], h [Hours], m [Minutes], s [Seconds and] SSS [Miliseconds]')
        }
      </div>
    )
  }
}

export default CountdownTimer

Okay, Let's see if that's any better...

Delta Math Only Throttle CPU Performance Screenshot

Okay, if I look really close, I can see a difference between the two... Those pink bars are a bit shorter, and it looks like there is a little less load on the CPU - but basically, this is the same as the first.

But wait!!! what about shouldComponentUpdate()?!

Delta Math and shouldComponentUpdate()

Code:

import React, { Component } from 'react'

import moment from 'moment'

class CountdownTimer extends Component {
  constructor(props) {
    super(props)

    this.state = {
      startTime: null,
      endTime: null,
      loadTime: null,
      oldDelta: null,
      delta: null,
      throttleFPS: null,
      throttleInterval: null,
      throttleTime: null,
      throttleDelta: null,
      difference: null,
      animationFrame: null
    }
  }
  componentWillMount() {
    this.setState({
      startTime: this.props.start,
      endTime: this.props.end,
      loadTime: new Date(),
      throttleFPS: 30,
      throttleTime: performance.now()
    })
  }
  componentDidMount() {
    // Need to wait for FPS to be set in componentWillMount() to set throttleInterval
    this.setState({
      throttleInterval: 1000/this.state.throttleFPS
    })
    this.tick(this.state.throttleTime)
  }
  tick(throttleTick) {
    let {
      throttleDelta,
      throttleTime,
      throttleInterval
    } = this.state

    throttleDelta = throttleTick - throttleTime

    if (throttleDelta > throttleInterval) {
      this.setState({
        throttleTime: throttleTick - (throttleTime % throttleInterval),
        delta: new Date(+new Date() - this.state.loadTime),
      })
    }

    this.setState({
      animationFrame: requestAnimationFrame(throttleTick => this.tick(throttleTick))
    })
  }

  shouldComponentUpdate() {
    let {
      oldDelta,
      delta
    } = this.state
    if (oldDelta === null) {
      this.setState({
        oldDelta: delta
      })
      return true
    } else if (oldDelta < delta) {
      this.setState({
        oldDelta: delta
      })
      return true
    } else {
      return false
    }
  }

  render() {
    let {
      startTime,
      endTime,
      delta
    } = this.state
    return (
      <div>
        {
          moment(moment(endTime).diff(startTime - delta)).format('d [Days], h [Hours], m [Minutes], s [Seconds and] SSS [Miliseconds]')
        }
      </div>
    )
  }
}

export default CountdownTimer

And the screenshot:

Delta Math and shouldComponentUpdate() Throttle CPU Performance Screenshot

Meh, nothing to write home to mom about - and certainly not my client.

What if I combine both requestAnimationFrame() AND setTimeout()??

requestAnimationFrame() with setTimeout()

The code:

import React, { Component } from 'react'

import moment from 'moment'

class CountdownTimer extends Component {
  constructor(props) {
    super(props)

    this.state = {
      startTime: null,
      endTime: null,
      loadTime: null,
      delta: null,
      throttleFPS: null,
      throttleInterval: null,
      difference: null,
      animationFrame: null
    }
  }
  componentWillMount() {
    this.setState({
      startTime: this.props.start,
      endTime: this.props.end,
      loadTime: new Date(),
      throttleFPS: 30,
      throttleTime: performance.now()
    })
  }
  componentDidMount() {
    // Need to wait for FPS to be set in componentWillMount() to set throttleInterval
    this.setState({
      throttleInterval: 1000/this.state.throttleFPS
    })
    this.tick()
  }
  tick() {
    let {
      throttleInterval
    } = this.state
    
    setTimeout(() => {
      this.setState({
        delta: new Date(+new Date() - this.state.loadTime),
        animationFrame: requestAnimationFrame(() => this.tick())
      })
    }, throttleInterval)
  }

  render() {
    let {
      startTime,
      endTime,
      delta
    } = this.state
    return (
      <div>
        {
          moment(moment(endTime).diff(startTime - delta)).format('d [Days], h [Hours], m [Minutes], s [Seconds and] SSS [Miliseconds]')
        }
      </div>
    )
  }
}

export default CountdownTimer

And at long last, the CPU profile screenshot:

requestAnimationFrame and setTimeout Throttle CPU Performance Screenshot

Okay, That's much better - don't you think?


What next?

Now, what about propTypes? Should I put them in? Can I check for a Date()? Or is that just a String?

My next big enhancement will be to somehow use redux to link all of the countdown timers on a single page together and see how that performance is against having them run their own state. The page I am building will have two of them running - but only one of them on the screen at any time. Maybe it would be better to "detect" if the componant is on the screen somehow and if it's not - than turn the animation off by canceling the requestAnimationFrame() Any thoughts or code critiques are welcome. I hope this helps somebody out there.


Edit:

So... I had the math reversed for the countdown timer... I've changed it above in all the code to moment(endTime).diff(startTime - delta)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment