DEV Community

Cover image for Mastering React Optimization Techniques: Boost Your App's Performance
Delia
Delia

Posted on • Updated on

Mastering React Optimization Techniques: Boost Your App's Performance

Hey there, fellow developers! 👋

If you're here, you probably love working with React just as much as I do. But let's be honest, no matter how much we love it, performance can sometimes be a real pain point. So today, we’re going to dive deep into some essential React optimization techniques to help you boost your app’s performance. Ready? Let’s get started! 🚀

Why Optimize React Apps?

First things first, why should we even bother with optimization? Well, performance matters—a lot. Users today expect lightning-fast load times and smooth interactions. If your app lags, users will leave. Plus, a well-optimized app can save on resource costs and improve your app’s SEO. So, let's make your React app the best it can be!

1. Use React.memo

React components re-render by default whenever their parent component re-renders. This can lead to unnecessary renders and slow down your app. That’s where React.memo comes in handy.

What is React.memo?

React.memo is a higher-order component that prevents a component from re-rendering if its props haven't changed. It’s like magic for your functional components!

Example

Here’s a simple example:

import React from 'react';

const MyComponent = ({ data }) => {
  console.log("Rendering MyComponent");
  return <div>{data}</div>;
};

export default React.memo(MyComponent);
Enter fullscreen mode Exit fullscreen mode

In this example, MyComponent will only re-render if the data prop changes. If data stays the same, React will skip the render, saving precious milliseconds.

2. Use useCallback and useMemo

These hooks are lifesavers when it comes to preventing unnecessary re-renders and calculations.

useCallback

useCallback returns a memoized version of a callback function that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

Example

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

const Button = React.memo(({ handleClick }) => {
  console.log("Rendering Button");
  return <button onClick={handleClick}>Click me</button>;
});

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

  const increment = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Button handleClick={increment} />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example, the Button component will not re-render unless the increment function changes, which only happens if the dependencies of useCallback change.

useMemo

useMemo returns a memoized value and recomputes it only when one of its dependencies changes. It's perfect for expensive calculations that shouldn't run on every render.

Example

import React, { useState, useMemo } from 'react';

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

  const expensiveCalculation = useMemo(() => {
    console.log("Running expensive calculation");
    return count * 2;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Result: {expensiveCalculation}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Here, the expensive calculation will only run when count changes, rather than on every render.

3. Code Splitting with React.lazy and Suspense

Code splitting is an optimization technique that allows you to split your code into various bundles, which can then be loaded on demand. This can drastically reduce the initial load time of your app.

Example

import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

const App = () => (
  <div>
    <h1>My React App</h1>
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  </div>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example, LazyComponent is only loaded when it’s needed, rather than being included in the main bundle. This can significantly improve your app’s load time.

4. Avoid Anonymous Functions in JSX

Anonymous functions in JSX can lead to performance issues because they create a new function on every render, causing unnecessary re-renders of child components.

Example

Instead of doing this:

<button onClick={() => handleClick()}>Click me</button>
Enter fullscreen mode Exit fullscreen mode

Do this:

const handleClick = () => {
  // Your logic here
};

<button onClick={handleClick}>Click me</button>
Enter fullscreen mode Exit fullscreen mode

This way, the handleClick function is not recreated on every render.

5. Optimize Component Mounting

Use componentDidMount and useEffect wisely to handle side effects and data fetching. Ensure these operations don’t block the initial rendering of the component.

Example

import React, { useEffect, useState } from 'react';

const DataFetchingComponent = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => setData(data));
  }, []);

  return (
    <div>
      {data ? <div>{data}</div> : <div>Loading...</div>}
    </div>
  );
};

export default DataFetchingComponent;
Enter fullscreen mode Exit fullscreen mode

Here, data fetching is handled in useEffect, ensuring it doesn’t block the initial render.

6. Reduce Reconciliation with shouldComponentUpdate and React.PureComponent

For class components, use shouldComponentUpdate to prevent unnecessary re-renders. Alternatively, you can use React.PureComponent, which does a shallow comparison of props and state.

Example

import React, { PureComponent } from 'react';

class MyComponent extends PureComponent {
  render() {
    return <div>{this.props.data}</div>;
  }
}

export default MyComponent;
Enter fullscreen mode Exit fullscreen mode

Using PureComponent ensures MyComponent only re-renders if props.data changes.

7. Use Immutable Data Structures

Using immutable data structures can make your state management more predictable and help prevent unnecessary re-renders. Libraries like Immutable.js can be very helpful here.

Example

import { Map } from 'immutable';

const initialState = Map({
  count: 0,
});

const increment = (state) => state.update('count', count => count + 1);

let state = initialState;
state = increment(state);
console.log(state.get('count')); // 1
Enter fullscreen mode Exit fullscreen mode

8. Optimize Lists with Virtualization

Rendering large lists can be a performance bottleneck. React Virtualized or React Window can help by rendering only the visible items in a list.

Example

import React from 'react';
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const App = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example, only the visible rows are rendered, reducing the rendering workload and improving performance.

9. Debounce Input Handlers

If you have input fields that trigger re-renders or data fetching on every keystroke, consider debouncing the input handlers to limit the number of updates.

Example

import React, { useState } from 'react';
import debounce from 'lodash.debounce';

const SearchInput = () => {
  const [query, setQuery] = useState('');

  const handleSearch = debounce((value) => {
    // Fetch data or perform search
    console.log('Searching for:', value);
  }, 300);

  const handleChange = (e) => {
    setQuery(e.target.value);
    handleSearch(e.target.value);
  };

  return <input type="text" value={query} onChange={handleChange} />;
};

export default SearchInput;
Enter fullscreen mode Exit fullscreen mode

In this example, the handleSearch function is debounced to limit the number of times it runs, improving performance.

10. Lazy Load Images and Components

Lazy loading images and components can significantly improve the initial load time of your application.

Example

For images, you can use the loading attribute:

<img src="image.jpg" alt="Example" loading="lazy" />
Enter fullscreen mode Exit fullscreen mode

For components, use React.lazy:

import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

const App = () => (
  <div>
    <h1>My React App</h1>
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  </div>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

Conclusion

Optimizing your React app doesn’t have to be daunting. By incorporating these techniques, you can significantly improve your app’s performance and provide a smoother experience for your users. Remember, every millisecond counts!

What optimization techniques have you found most useful? Let me know in the comments below! And if you found this post helpful, feel free to share it with your fellow developers. Happy coding! 🚀

I hope you enjoyed this article! Feel free to ask any questions or share your thoughts in the comments. Let’s keep the conversation going and support each other in building high-performance React applications!

Twitter: @delia_code

Instagram:@delia.codes

Blog: https://delia.hashnode.dev/

Top comments (3)

Collapse
 
belyas profile image
yassine belkaid

Can you please elaborate more why having the function declared outside of onClick would make any difference, in re-render, the declared function will be recreated as well and then passed to onClick handler, so not sure where the optimization here occurs unless we are introducing useCallback to memorize this function

const handleClick = () => { // Your logic here};
<button onClick={handleClick}>Click me</button>

Collapse
 
delia_code profile image
Delia

In the case of the inline function, the function is created every time the component re-renders. This means a new instance of the function is generated during each render cycle. While for many applications this might not cause a noticeable performance issue, in scenarios with frequent re-renders or a large number of components, this can become inefficient.

Declaring the function outside of the render context ensures that the function is not recreated during each render so the same function instance is reused. This can reduce the overhead associated with frequent re-renders because the handleClick function reference remains stable. Creating functions on each render cycle adds to the performance overhead, especially in complex or frequently updated components.

Collapse
 
belyas profile image
yassine belkaid

Thanks @delia_code for the reply.

Actually that's not true, declaring a function outside of JSX doesn't make any different, it'd be the same thing as during the re-rendering the declared function would be re-created with new reference, which causes child component to re-render even if wrapped with a memo, please have a look at this simple example which mimic this scenario: codesandbox.io/p/sandbox/parent-ch...

Please open uo devtool -> Console, and see the log