noguzo blog

dispatchするアクションはできる限りまとめたほうがいいのか

2019.10.22


先日コードレビューをしていたとき。 APIを呼び出し、データを取得しておく間ローディングを表示する実装があった。

その際、APIを呼ぶ前にローディングのスタータス更新と、それ以外に必要なステータス更新を別のアクションで行っていた。 ↓のような感じ。

// Action Creator

  const updateIsLoading = createAction(UPDATE_IS_LOADING)
  const updateHasMore = createAction(UPDATE_HAS_MORE)

// ..............................

  dispatch(updateIsLoading(true))
  if (SomeBoolCondition) {
    dispatch(updateHasMore(true))
  }

// ..............................

// Reducer
export default handleActions(
  {
    [UPDATE_IS_LOADING]: (state, { payload }) => ({
      ...state,
      isLoading: payload
    }),
    [UPDATE_HAS_MORE]: (state, { payload }) => ({
      ...state,
      hasMore: payload
    })
  }
)

個人的に無駄にState更新が走ってしまいそうなので、このような場合はアクションをまとめている。 ただ、今回レビューしたプロジェクトだとアクションの粒度がかなり細かくすでに設定されていたためこれでもいいかなとも感じた。

そこで、実際パフォーマンスに影響はあるのか調べてみた。

検証

ボタンを2つ用意した。

1つ目は、2つのstate更新を2つのアクションで行う

2つ目は、2つのstate更新を1つのアクションで行う

buttons.png

これを10回行った結果がこちら。

Multi(2つのアクション) Once(1つのアクション)
1回目 0.9900 0.2050
2回目 0.1400 0.1000
3回目 0.2100 0.1150
4回目 0.1700 0.1100
5回目 0.1600 0.1100
6回目 0.1850 0.1150
7回目 0.1450 0.1000
8回目 0.1450 0.0950
9回目 0.1700 0.0900
10回目 0.1550 0.0950
平均 0.2470(ms) 0.1135(ms)

平均で、0.13msほどの差がでた。

RAILモデルでは、ボタンなどのクリック操作に対して100ms以内にレスポンスを返すことができれば、ユーザが遅れを感じないとされています。

例えば、タブを切り替えたときAPIでデータを取得し、結果を表示するまで100ms以内です。

こう考えると0.1msでも短縮できるところは短縮しておきたい。

結論

やはり、当たり前のことですが、省略できるアクションのdispatchはパフォーマンス的に省略したほうが良いです。

検証に使ったコード

  • コンポーネント
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { sometAction, sometAction2, sometActionTogether} from '../redux'

export default () => {
  const { isSomething, isSomething2 } = useSelector(state => ({ ...state }))
  const dispatch = useDispatch()
  const [time, setTime] = useState(0)

  return (
    <div style={{ marginTop: 100 }}>
      <button onClick={() => {
        const localTime = mesureFunc(() => {
          dispatch(sometAction(!isSomething))
          dispatch(sometAction2(!isSomething2))
        })
        setTime(localTime)
      }} >Multi dispatch</button>

      <button onClick={() => {
        const localTime = mesureFunc(() =>
          dispatch(sometActionTogether({ isSomething: !isSomething, isSomething2: !isSomething2 })
        ))
        setTime(localTime)
      }} >dispatch once</button>

      {(time > 0) && <div>{time}</div>}
    </div>
  )
}

const mesureFunc = (targetFunc, times = 1) => {
  const start = performance.now();

  [...new Array(times)].forEach(targetFunc)
  const end = performance.now()
  const time = (end - start).toFixed(4)
  console.log(time)
  return time
}
  • redux
import { handleActions, createAction } from 'redux-actions'

// Actions
const SOME_ACTION = 'someAction'
const SOME_ACTION2 = 'someAction2'
const SOME_ACTION_TOGETHER = 'someActionTogether'

// Action Creators
export const sometAction = createAction(SOME_ACTION)
export const sometAction2 = createAction(SOME_ACTION2)
export const sometActionTogether = createAction(SOME_ACTION_TOGETHER)

// Initial State
const initialState = {
  isSomething: false,
  isSomething2: false
}

// Reducer
export default handleActions(
  {
    [SOME_ACTION]: (state, { payload }) => ({
      ...state,
      isSomething: payload
    }),
    [SOME_ACTION2]: (state, { payload }) => ({
      ...state,
      isSomething2: payload
    }),
    [SOME_ACTION_TOGETHER]: (state, { payload }) => ({
      ...state,
      isSomething: payload.isSomething,
      isSomething2: payload.isSomething2
    })
  },
  initialState
)

おしまい。


Frontend engineer.