2025-12-20
ReactJS
00
请注意,本文编写于 212 天前,最后修改于 211 天前,其中某些信息可能已经过时。

目录

Redux Performance Optimization(old)
The default case of a reducer returns the original state
Precise mapStateToProps (select only the fields you need)
Use shallowEqual for shallow comparison
React performance optimization supplement
React.memo prevents unnecessary re-rendering
Code Splitting
Route lazy loading
Long list optimization
Basic Usage of @reduxjs/toolkit
Toolkit Performance Optimization
shallowEqual
use createSelector to memory selector
the second parameter of redux's createStore
Performance analysis tools

Today I learned how to use @reduxjs/toolkit, as well as Redux-related performance optimizations.

Redux Performance Optimization(old)

The following is the best pratices of Redux Reducer immutability.

The default case of a reducer returns the original state

The default case of a reducer returns the original state, which can avoid other components re-rendering

js
const PersonReducer = (state = { age: 12, name: "zs" }, action) => { switch (action.type) { case "P_ADD": // ✅ When the `state` actually changes, a new object must be created (immutability principle). return { ...state, age: state.age + 1 }; default: // ✅ When the `state` has not changed, returning a reference to the original state is the correct approach // This is the best practice recommended by Redux, for the following reasons: // 1. Redux uses shallow equality(浅比较) to determine if the state has changed. // 2. If the references are the same, it means there's no change, and unnecessary re-rendering won't be triggered. // 3. Creating a new object every time would cause all connected components to re-render (performance issue). // 4. The core of immutability is "not modifying the original object," not "always creating a new object." // 不可变性的核心是"不修改原对象",而不是"总是创建新对象" return state; // ❌ other components will re-render // such CounterComp, which is introduced CounterReducer // return {...state} } };

Why do this?

  • Redux uses a shallow comparison (Object.is()) to determine if the state has changed.
  • If the references are the same, it means there have been no changes and a re-render will not be triggered.
  • This is the official recommended practice of Redux and is also key to performance optimization.

📚 Core Principles

--
The essence of immutability:It avoids modifying the original object, instead of always creating a new one.
When to create a new object:Only when the state truly changes.
When to return the original reference:Returns the original reference when the state has not changed.
Redux's comparison mechanism:Uses shallow comparison (reference comparison), not deep comparison.

Precise mapStateToProps (select only the fields you need)

Issue: If mapStateToProps returns the entire state, any change to the state will trigger a component re-render.

Optimization: Only select the fields the component actually needs.

js
// ❌ Error: Returning the entire state const mapStateToProps = (state) => state; // ✅ Correct: Select only the fields you need. const mapStateToProps = (state) => ({ count: state.counter, // only select `counter` });

Use shallowEqual for shallow comparison

Issue: By default, connect uses strict equality (===) comparisons, while shallow comparisons are required for objects.

Optimization: Use shallowEqual for shallow comparison.

typescript
import { shallowEqual } from "react-redux"; const PersonComp = connect(mapStateToProps, mapDispatchToProps, null, { areStatesEqual: shallowEqual, areOwnPropsEqual: shallowEqual, })(Component);

Effect: The object is only re-rendered when its properties actually change.

TODO check it

React performance optimization supplement

React.memo prevents unnecessary re-rendering

Issue: When the parent component re-renders, the child component also re-renders (even if the props haven't changed).

Optimization: Use React.memo to wrap the component.

js
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) { return <div>{data.value}</div>; }); // Or define a custom comparison function. const ExpensiveComponent = React.memo( function ExpensiveComponent({ data }) { return <div>{data.value}</div>; }, (prevProps, nextProps) => { // Custom comparison logic return prevProps.data.value === nextProps.data.value; } );

Effect: Only re-render when props actually change.

Code Splitting

Issue: All the code is packaged together, resulting in a slow initial load.

Optimization: Using React.lazy and Suspense to split code。

typescript
import React, { Suspense, lazy } from "react"; // Dynamically imported components const LazyComponent = lazy(() => import("./LazyComponent")); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> ); }

Effect: Load on demand to reduce the initial package size.

Route lazy loading

js
const UseStateUsage = lazy(() => import("./hook/UseStateUsage")); // 加载状态组件 const Loading = () => ( <div style={{ padding: "50px", textAlign: "center" }}> <div>🔄 加载组件中...</div> </div> ); const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<BrowserRouter> {/* 关键:必须用 Suspense 包裹懒加载组件 */} <Suspense fallback={<Loading />}> <Routes> <Route path="/UseStateUsage" element={<UseStateUsage />}></Route> {/* 这里也可以放置非懒加载的路由 */} </Routes> </Suspense> </BrowserRouter>)

Long list optimization

Issue: Rendering long list items can cause performance issues.

Optimization: Use the virtualization library to render only the visible items.

js
import { FixedSizeList } from "react-window"; function VirtualizedList({ items }) { return ( <FixedSizeList height={600} itemCount={items.length} itemSize={50} width="100%" > {({ index, style }) => <div style={style}> {items[index].name} </div>} </FixedSizeList> ); }

Effect: Rendering only the visible area and nearby elements significantly improves performance.

Basic Usage of @reduxjs/toolkit

References

Getting Started with Domo

  • store.ts
ts
import { configureStore } from "@reduxjs/toolkit"; import { CounterReducer } from "./counterSlice"; // this is a wrapper around the basic Redux Create Store Function // It autumatically sets up a store with right defaults. // For example, it automatically turns on the Redux Dev Tools Extensions. // it automatically adds the Thunk middleware // and automatically turns on a couple of development checks that catch common mistakes, // like accidental mutations. export const store = configureStore({ // @reduxjs/toolkit has also do combineReducers automatically reducer: { counter: CounterReducer, }, }); export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof store.getState>;
  • counterSlice.ts
ts
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; interface ICounterState { value: number; } const initialState: ICounterState = { value: 0 }; const counterSlice = createSlice({ name: "counter", initialState, reducers: { incremented(state) { // it's okay to do this, beacuse immer.js makes it immutable state.value += 1; }, amountAdded(state, actions: PayloadAction<number>) { state.value += actions.payload; }, }, }); export const { incremented, amountAdded } = counterSlice.actions; export const CounterReducer = counterSlice.reducer; import { type Dispatch } from "redux"; export const incrementAsync = (amount: number) => (dispatch: Dispatch) => { setTimeout(() => { dispatch(amountAdded(amount)); }, 1000); };
  • hooks.ts
ts
import { type TypedUseSelectorHook, useSelector, useDispatch, } from "react-redux"; import type { AppDispatch, RootState } from "./store"; export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
  • app.tsx usage
tsx
import { Provider } from "react-redux"; import { store } from "./store"; import { incremented, amountAdded, incrementAsync } from "./counterSlice"; import { useAppDispatch, useAppSelector } from "./hooks"; export default function App() { return ( <Provider store={store}> <Counter /> </Provider> ); } function Counter() { const counter = useAppSelector((state) => state.counter); const dispatch = useAppDispatch(); function handleClick() { dispatch(incremented()); } return ( <div> <p>{counter.value}</p> <button type="button" onClick={handleClick}> ADD- </button> <button type="button" onClick={() => dispatch(amountAdded(5))}> ADD by 5 </button> <button type="button" onClick={() => dispatch(incrementAsync(2))}> Async ADD 2 </button> </div> ); }

Q: Why we should use @reduxjs/toolkit?

The Explanation from redux's maintainer

  1. connect is really really hard to use with Typescript
    and "useSelector" "useDispatch" are trivial
  2. In older versions of Redux, we couldn't modify existing state because the UI wouldn't render. So in toolkit we use immer.js, it actually one kind of replays them and turns it into a safe and correct, immutable update, as if we'd done all of that copying and spreading and mapping and everything ourselves(但是使用了 Redux Toolkit, 它使用了 immer.js,它包裹了 state 更新和追踪所有 mutations, 实际上,它以一种特殊的方式重放这些画面,并将其转化为安全且正确的画面。)
对比维度传统 ReduxRedux Toolkit(RTK)
TS 支持需手动写大量类型定义,繁琐原生深度适配,自动推导类型,零冗余类型代码
核心依赖需手动安装 thunkimmer内置 thunkimmerreselect,零配置开箱即用
模板代码量多(3 + 文件 / 状态模块)极少(1 文件 / 状态模块,createSlice 整合所有)
不可变更新手动解构 / Object.assign,易出错直接修改 draftimmer 自动转不可变,无学习成本
异步逻辑处理手动定义异步 action,复杂createAsyncThunk+extraReducers,优雅处理异步状态
调试体验需手动配置 devtools内置适配,直接接入,支持异步状态回溯
团队协作成本高(规范难统一)低(强制最佳实践,代码风格统一)

supplement: redux chrome devtools

Toolkit Performance Optimization

shallowEqual

tsx
import React from "react"; import { useSelector, shallowEqual } from "react-redux"; import { type RootState } from "./store"; const UserCounterDisplay: React.FC = () => { const selectorResult = useSelector( (state: RootState) => ({ username: state.user.username, isLoggedIn: state.user.isLoggedIn, // count: state.counter.value, }), // if this has no shallowEqual, when the state.counter.value changes, this component will be rerender // if this has shallowEqual, it will not rerender when state.counter.value is changed shallowEqual // ← key to performance optimization ); console.log("UserCounterDisplay 渲染了!", selectorResult); return ( <div style={{ padding: "20px", border: "2px solid #4caf50", margin: "10px 0", borderRadius: "8px", }} > <h3>用户信息与计数器(使用了 shallowEqual)</h3> <p>用户名:{selectorResult.username}</p> <p>登录状态:{selectorResult.isLoggedIn ? "已登录" : "未登录"}</p> {/* <p>计数:{selectorResult.count}</p> */} </div> ); }; export default UserCounterDisplay;

use createSelector to memory selector

Continue to supplement the cases based on the previous examples.

  • UserCounterDisplay.tsx
tsx
const UserCounterDisplay: React.FC = () => { const doubleCount = useSelector((state: RootState) => { // if counter hasn't change ,but user has change, // this selector will also exec console.log("doubleCount exec"); return state.counter.value * 2; }); return <div> <p>double count:{doubleCount}</p> </div> }

after optimization

  • UserCounterDisplay.tsx
tsx
import { doubleCountSelector } from "./counterSlice"; const UserCounterDisplay: React.FC = () => { const doubleCount = useSelector(doubleCountSelector); return <div> <p>double count:{doubleCount}</p> </div> }
  • counterSlice.ts
ts
const count = (state: RootState) => state.counter.value; export const doubleCountSelector = createSelector([count], (count) => { // if counter hasn't change ,but user has change, // this selector will not exec console.log("doubleCountSelector exec", count); return count * 2; });

Advanced usage

image.png

image.png

supplement: in old version of redux , we should install reselect(npm install reselect)

js
import { createSelector } from "reselect"; const selectCounter = (state) => state.counter; // 记忆化选择器:只有当 counter 改变时才重新计算 const selectCounterDouble = createSelector( [selectCounter], (counter) => counter * 2 ); const mapStateToProps = (state) => ({ doubleCount: selectCounterDouble(state), });

the second parameter of redux's createStore

The complete syntax of createStore

js
import { createStore } from 'redux'; const store = createStore(reducer, preloadedState, enhancer);

usage

js
// reducer.js const initialState = { count: 0, name: '默认名称' }; // reducer 默认初始状态 function rootReducer(state = initialState, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; default: return state; } } // store.js // 预加载状态(结构需与 reducer 状态一致) const preloadedState = { count: 10, name: '预加载名称' }; const store = createStore(rootReducer, preloadedState); console.log(store.getState()); // 输出:{ count: 10, name: '预加载名称' }(覆盖了 reducer 默认初始状态)

Performance analysis tools

  • React DevTools Profiler: Analyze component rendering performance
  • Redux DevTools: Analyzing changes in Redux state
  • Chrome Performance: Analyze overall performance

本文作者:郭郭同学

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!