React Performance and Optimization

This blog explores React performance optimization techniques, focusing on memorization strategies for components, values, and functions.

React

Web Performance

Javascript

Web Development

Frontend Optimization

Table of Contents

1. Code Splitting

Don't download code until that user needs it.

1. Dynamic Module Import import

For example, in the registerModule file, the module file is only downloaded when the user needs to register.

registerModule.js

const register = (formData) => {
  console.log(formData)
}

export { register }

App.js

import('./modules/registerModule')
  .then((module) => module.register(formData))
  .catch((err) => console.log(err))

2. React.Lazy

Server-side rendering is not supported

Components that require lazy loading must be wrapped with <Suspense>. The fallback prop, which defines the element to render while the component is loading, is required (it can be null, but must be explicitly specified).

In the demo below, if the user has not visited the "This Season's Ranking" and "Now Streaming" pages, these files will not be downloaded. The corresponding chunk is only downloaded when the user navigates to these pages.

const Home = lazy(() => import('./pages/Home')) // import Home from './pages/Home'
const Air = lazy(() => import('./pages/Air'))
const Rank = lazy(() => import('./pages/Rank'))

const App = () => {
  return (
    <Router>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/air">On Air</Link>
        <Link to="/rank">Rank</Link>
      </nav>

      <Suspense fallback={<Loading />}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/air" component={Air} />
          <Route exact path="/rank" component={Rank} />
        </Switch>
      </Suspense>
    </Router>
  )
}

3. Memorization & Rendering

1. React.memo()

Component Memorization: A component re-renders if its props change; otherwise, the previous render is used.

The following demo uses the <SnowBackground /> component to render 1,000 snowflake particles as a background.

Each time the <input /> field updates, the <App /> component re-renders. Since <SnowBackground /> is a child component, it also re-renders.

With 1,000 particles, rendering can cause slight lag. More importantly, if <SnowBackground /> props haven't changed, there is no need to re-render the component. As the number of particles increases, frequent re-renders can negatively impact user experience.

const SnowBackground = React.memo(() => {
  console.log('Render Snow Particles')
  return <Snow backgroundColor="#000" particles={1000} />
})

const App = () => {
  console.log('Render App')

  const handleChange = (e) => setSearch(e.target.value)

  return (
    <>
      <input type="text" value={search} onChange={handleChange} />
      <SnowBackground />
    </>
  )
}

export default App

2. useMemo()

Value Memorization: A function is re-executed only if its dependencies change.

1. Recomputing Expensive Functions Based on Dependency Changes

calculate() is a computationally expensive function, but toggling the <button> to switch themes also causes the <App /> component to re-render, triggering calculate() again—even though the toggle is unrelated to this function.

To optimize this, useMemo stores the return value of calculate(). If number remains unchanged, the memoized value is used; otherwise, calculate() is re-executed.

2. Referential Equality for Storing Reference Values

Similarly, themeStyle is an object. Every <App /> re-render creates a new themeStyle object. Since objects in JavaScript are reference values, React's shallow comparison detects it as a new object, causing unnecessary re-renders whenever the input changes.

const calculate = (n) => {
  for (let i = 0; i < 1000000000; i++) {}
  console.log('计算')
  return n + 2
}

const App = () => {
  const [number, setNumber] = useState(0)
  const [dark, setDark] = useState(false)
  const result = useMemo(() => {
    return calculate(number)
  }, [number])

  const themeStyle = useMemo(() => {
    return {
      backgroundColor: dark ? 'black' : 'white',
      color: dark ? 'white' : 'black'
    }
  }, [dark])

  return (
    <>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
      />
      <button onClick={() => setDark((prevState) => !prevState)}>
        Change Theme
      </button>
      <div style={themeStyle}>{result}</div>
    </>
  )
}

export default App

3. useCallback()

Function Memorization: Typically used with React.memo() to prevent unnecessary function re-renders.

1. Scenario: useCallback() + React.memo() to Prevent Component Re-renders

In the following code, the child component <Child /> uses memo to prevent unnecessary re-renders. It also receives an increment function from the parent component to update the counter.

However, memo does not work as expected here. Ideally, clicking the button should only re-render the parent, while <Child />—wrapped in memo—should compare props and decide whether to update. Since only count changes, and increment() remains the same, the child component should not re-render.

The issue arises because, in JavaScript, functions are objects, and due to Referential Equality, two identical objects are still considered different references. This causes memo to detect a new function reference and re-render <Child /> unnecessarily.

Previously, we used useMemo to resolve Referential Equality issues, but:

  • useMemo() returns a value, useful for caching expensive computations.
  • useCallback() returns a function, which can accept parameters and be used for event handling.

Thus, by combining useCallback() with memo, we can prevent both redundant function re-creations and unnecessary child component re-renders.

import React, { useCallback, useState } from 'react'

const Child = React.memo((props) => {
  const { increment } = props
  return (
    <>
      <button onClick={() => increment(5)}>Increment</button>
    </>
  )
})

const App = () => {
  const [count, setCount] = useState(0)

  const increment = useCallback((step) => {
    setCount((prevState) => prevState + step)
  }, [])

  return (
    <>
      <h1>Count: {count}</h1>
      <Child increment={increment} />
    </>
  )
}

export default App