【React】HOCのレンダリングコスト
何をしたか
React で HOC (Higher Order Component) が肥大化するとレンダリングのコストが増大するはずだと思って調べた。
もっと詳しく
HOC は要するに高階関数であり、高階関数はロジックを抽象化するために優れたデザインパターンだが、パフォーマンス面では関数呼び出しの回数が増えるため、コンポーネントをレンダリングするのに要する時間が増す。それはやむを得ないが、問題はどのくらい影響するかである。
そこで、単純な Counter コンポーネントを作り、それを HOC でたくさんラップしたらどのくらいレンダリング速度が低下するかを調べた。
登場人物を挙げる。Counter コンポーネント。
import {pure} from 'recompose' const Counter = pure(({onCountUp, count}) => ( <div className='counter'> <button onClick={onCountUp}>count up</button> <div>count: {count}</div> </div> ))
何もしない HOC。
import React, {Component} from 'react' const withNoop = (BaseComponent) => { class WithNoop extends Component { render () { return <BaseComponent {...this.props} /> } } return WithNoop }
何もしない HOC を繰り返し適用できるようにする。
const withNoops = (repeat) => (BaseComponent) => { const withNoopArray = new Array(repeat).fill(withNoop) return withNoopArray.reduce( (Component, withNoop) => withNoop(Component), BaseComponent ) }
HOC でラップしまくった Counter コンポーネント。とりあえず 10000 回ほどラップする。
const HighCostCounter = pure(
withNoops(10000)(Counter)
)
そして、メインとなる親コンポーネント。
let timerId = 1 class App extends Component { constructor (props) { super(props) this.state = { countA: 0, countB: 0 } this.onCountUpA = this.onCountUpA.bind(this) this.onCountUpB = this.onCountUpB.bind(this) } render () { const {countA, countB} = this.state return ( <div className='App'> <Counter onCountUp={this.onCountUpA} count={countA} /> <HighCostCounter onCountUp={this.onCountUpB} count={countB} /> </div> ) } onCountUpA () { this.setState({countA: this.state.countA + 1}) } onCountUpB () { this.setState({countB: this.state.countB + 1}) } // state をアップデートするたびにかかった時間を計測する componentWillUpdate () { console.time(timerId) } componentDidUpdate () { console.timeEnd(timerId) timerId++ } }
比較した結果
単純な Couner コンポーネントのカウントアップボタンを押すと、親コンポーネントのアップデートが終わるまで平均約 1 msだった。
HOC でラップしまくった HighCostCounter のほうは、平均約 150 ms だった。まあ 10000 回もラップすることは現実のアプリケーションではありそうもないので、100 回のラップで試すと平均約 8 ms だった。
うん、あんまり気にしなくて良さそう。確かに HOC で抽象化することでレンダリングのオーバーヘッドは増すけど、パフォーマンスに深刻な影響はなさそう。
とはいえ props が大きくなってくるとまた違うかもしれないけど。
コード全体
import React, { Component } from 'react' import {pure} from 'recompose' // --- HOC --- const withNoop = (BaseComponent) => { class WithNoop extends Component { render () { return <BaseComponent {...this.props} /> } } return WithNoop } const withNoops = (repeat) => (BaseComponent) => { const withNoopArray = new Array(repeat).fill(withNoop) return withNoopArray.reduce( (Component, withNoop) => withNoop(Component), BaseComponent ) } // --- Components --- const Counter = pure(({onCountUp, count}) => ( <div className='counter'> <button onClick={onCountUp}>count up</button> <div>count: {count}</div> </div> )) const HighCostCounter = pure( withNoops(10000)(Counter) ) let timerId = 1 class App extends Component { constructor (props) { super(props) this.state = { countA: 0, countB: 0 } this.onCountUpA = this.onCountUpA.bind(this) this.onCountUpB = this.onCountUpB.bind(this) } render () { const {countA, countB} = this.state return ( <div className='App'> <Counter onCountUp={this.onCountUpA} count={countA} /> <HighCostCounter onCountUp={this.onCountUpB} count={countB} /> </div> ) } onCountUpA () { this.setState({countA: this.state.countA + 1}) } onCountUpB () { this.setState({countB: this.state.countB + 1}) } componentWillUpdate () { console.time(timerId) } componentDidUpdate () { console.timeEnd(timerId) timerId++ } } export default App