NashTech Blog

20 Tips to optimize react app performance

Table of Contents

1. Use local state instead keep state of child components in parent component.

State in React is a built-in object that stores data that changes over time and affects what is rendered on the screen. It allows your component to be dynamic and interactive, like updating text as the user types, opening a modal, or switching tabs. When a parent component holds too much state (including child states), it re-renders all children even when only one child’s state changes.

```js
//SHOULD NOT
import { useState } from "react";

export default function App() {
  const [input, setInput] = useState("");

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      <h3>Input text: {input}</h3>
      <ChildComponent />
    </div>
  );
}

function ChildComponent() {
  console.log("child component is rendering");
  return <div>This is child component.</div>;
};
```

```js
//SHOULD
export const ShouldComponent = () => {
  return (
    <>
      <FormInput />
      <ChildComponent />
    </>
  );
};

function FormInput() {
  const [input, setInput] = useState("");

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      <h3>Input text: {input}</h3>
    </div>
  );
}

function ChildComponent() {
  console.log("child component is rendering");
  return <div>This is child component.</div>;
}

2. Memoizing React components to prevent unnecessary re-renders

React.memo is a function that memoizes a React component, allowing it to re-render only when its props change. This helps prevent unnecessary re-renders when the parent component updates but the child’s props stay the same. As a result, it improves performance and keeps the UI smooth, especially for complex or frequently rendered components.

You don’t need to use React.memo for every component. If a component is simple, or its props change every time anyway, React.memo won’t help and might even make things slower. It’s best used only when a component is slow to render and its props usually stay the same — otherwise, you can skip it.

```js
export const WithMemoComponent = () => {
const [input, setInput] = useState("");
const [count, setCount] = useState(0);

return (
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={() => setCount(count + 1)}>Increment counter</button>
<h3>Input text: {input}</h3>
<h3>Count: {count}</h3>
<hr />
<WithMemoChildComponent count={count} />
</div>
);
};

const WithMemoChildComponent = React.memo(function WithMemoChildComponent({ count }: any) {
console.log(`Child component is rendering ${count}`);
return (
<div>
<h2>This is a child component.</h2>
<h4>Count: {count}</h4>
</div>
);
});
```

3. Memoizing functions which passed into component props

Think about a usecase when child component trigger a function in parent. This case parent will pass a function to child component. The child component will call this function. By passing this function directly or using memo does not stop react from rendering as memo does not memorize function. We need to useCallback.

```js
import React, { useState } from "react";

const expensiveFunction = (count) => {
// artificial delay (expensive computation)
for (let i = 0; i < 1000000000; i++) {}
return count * 3;
};

export default function App() {
// ...
const myCount = expensiveFunction(count);
return (
<div>
{/* ... */}
<h3>Count x 3: {myCount}</h3>
<hr />
<ChildComponent count={count} onClick={incrementCount} />
</div>
);
}

const ChildComponent = React.memo(function ChildComponent({ count, onClick }) {
// ...
});
```

4.Memoizing heavy functions

Memoizing heavy functions in React means storing the result of a slow or expensive calculation so it doesn’t have to run again on every render. We do this using useMemo to improve performance, making sure the function only runs again when its inputs actually change.

```js
const expensiveFunction = (count: number) => {
// artificial delay (expensive computation)
for (let i = 0; i < 1000000000; i++) { }
return count * 3;
};

const myCount = React.useMemo(() => {
return expensiveFunction(count);
}, [count]);

```

5. Code splitting in React using dynamic import()

Code splitting in React using import() allows you to load parts of your code only when needed, instead of including everything in one big bundle. This is done by dynamically importing components or modules, which helps improve performance by reducing the initial load time and speeding up page rendering for users.

Use React.lazy load to reduce number of js bundles downloaded. It will download when needed. For loading load components will use put into <Suspense/>


“`js
const WelcomePage = lazy(() => import(“@pages/WelcomePage”));
const LoginPage = lazy(() => import(“@pages/LoginPage”));

function App() {
return (
<div className=”App”>
{/* <SideBar></SideBar> */}
<Suspense fallback={<Loader></Loader>}>
<div>
<Routes>
<Route path=”/” element={<WelcomePage title=”Welcome to TudiTech” />} />
<Route path=”/login” element={<LoginPage title=”Login” />} />
<Route path=”/home” element={<HomePage title=”Home” />} />
</Routes>
</div>
<Copyright title=’TudiTech@2024’></Copyright>
</Suspense>
</div>
);
}

export default App;
“`

6. Windowing or list virtualization in React applications

Windowing (or list virtualization) in React is a technique to render only the visible items in a long list instead of rendering everything at once. This helps boost performance and reduce memory usage, especially when dealing with hundreds or thousands of items, by showing only what’s currently in the viewport.

Libraries like react-window or react-virtualized are commonly used for this:

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

<List height={400} itemCount={1000} itemSize={35} width={300}>
{({ index, style }) => <div style={style}>Item {index}</div>}
</List>

7. Use Lazy loading images

Lazy loading images means delaying the loading of images until they are actually visible on the screen. This helps reduce initial page load time and improve performance, especially when there are many images on a page.

```js
import { LazyLoadImage } from "react-lazy-load-image-component";
import "react-lazy-load-image-component/src/effects/blur.css";

export default function App() {
return (
<div className="App">
<LazyLoadImage
src={"https://placedog.net/500/300"}
width={600}
height={400}
alt="Image Alt"
effect="blur"
/>
</div>
);
}
```

8. Use immutable data structures

Using immutable data structures means avoiding direct changes to existing objects or arrays. Instead, you create new ones when updating state. This helps React detect changes more easily, leading to more predictable rendering and better performance, especially when comparing previous and next state.

```js
//By modifying bookInfo.name directly will not trigger render. The UI will not display new book name. See solution in <ModifyCopyStateComponent/> below.

export function ShouldNotModifyStateComponent() {
const [bookInfo, setBookInfo] = useState({
name: "A Cool Book",
noOfPages: 28
});

const updateBookInfo = () => {
bookInfo.name = 'A New title'
};
return (
<div className="App">
<h2>Update the book's info</h2>
<pre>
{JSON.stringify(bookInfo)}
</pre>
<button onClick={updateBookInfo}>Update</button>
</div>
);
}
```

```js
export function ModifyCopyStateComponent() {
const [bookInfo, setBookInfo] = useState({
name: "A Cool Book",
noOfPages: 28
});

const updateBookInfo = () => {
const newBookInfo = { ...bookInfo };
newBookInfo.name = "A Better Title";
setBookInfo(newBookInfo);
};
return (
<div className="App">
<h2>Update the book's info</h2>
<pre>
{JSON.stringify(bookInfo)}
</pre>
<button onClick={updateBookInfo}>Update</button>
</div>
);
}
```

9. Use web worker for heavy task

Using Web Workers in React allows you to run heavy or CPU-intensive tasks (like data processing, parsing, or calculations) in a separate background thread, so the main UI stays responsive and smooth. Without Web Workers, these tasks can block the main thread and cause the app to freeze or lag.

Let’s say you let users upload a large CSV file (thousands of rows), and you want to parse it into JSON for display or filtering. If you do this parsing directly in the main thread, the app might freeze while parsing.

//csvWorker.js
self.onmessage = function (e) {
  const csvText = e.data;
  const rows = csvText.split('\n').map((line) => line.split(','));
  self.postMessage(rows);
};

//CSVUploader.tsx

import React, { useState } from 'react';

export function CSVUploader() {
  const [data, setData] = useState<any[][]>([]);
  const [loading, setLoading] = useState(false);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = () => {
      const csvText = reader.result as string;
      const worker = new Worker(new URL('./csvWorker.js', import.meta.url));
      setLoading(true);

      worker.postMessage(csvText);
      worker.onmessage = (e) => {
        setData(e.data);
        setLoading(false);
        worker.terminate();
      };
    };
    reader.readAsText(file);
  };

  return (
    <div>
      <input type="file" accept=".csv" onChange={handleFileChange} />
      {loading && <p>Processing CSV...</p>}
      {!loading && data.length > 0 && <pre>{JSON.stringify(data.slice(0, 5), null, 2)}</pre>}
    </div>
  );
}

10. Use Reselect

Reselect is a library commonly used with Redux to create memoized selectors, which are functions that efficiently compute derived data from the Redux store. Instead of recalculating data on every state change, Reselect only recalculates when the input values actually change, helping to avoid unnecessary re-renders and improve performance.

```js
import { createSelector } from 'reselect'

interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}

const state: RootState = {
todos: [
{ id: 0, completed: false },
{ id: 1, completed: true }
],
alerts: [
{ id: 0, read: false },
{ id: 1, read: true }
]
}

const selectCompletedTodos = (state: RootState) => {
console.log('selector ran')
return state.todos.filter(todo => todo.completed === true)
}

selectCompletedTodos(state) // selector ran
selectCompletedTodos(state) // selector ran
selectCompletedTodos(state) // selector ran

const memoizedSelectCompletedTodos = createSelector(
[(state: RootState) => state.todos],
todos => {
console.log('memoized selector ran')
return todos.filter(todo => todo.completed === true)
}
)

memoizedSelectCompletedTodos(state) // memoized selector ran
memoizedSelectCompletedTodos(state)
memoizedSelectCompletedTodos(state)

console.log(selectCompletedTodos(state) === selectCompletedTodos(state)) //=> false

console.log(
memoizedSelectCompletedTodos(state) === memoizedSelectCompletedTodos(state)
) //=> true
```

11. Use FunctionComponent instead of ClassComponent.

Function components in React offer better performance because they are lightweight (no this binding or class instances), and they work seamlessly with React Hooks like useMemo or useCallback to control re-renders and avoid unnecessary calculations. Modern React is also optimized for function components, enabling advanced features like concurrent rendering and Suspense, which class components can’t fully support. This makes function components not just simpler to write, but also more efficient and future-proof.

//Function Component (Optimized with useCallback)
import React, { useCallback } from 'react';

const ProductItem = React.memo(({ product, onAddToCart }: any) => {
console.log('Rendered:', product.name);
return (
<div>
<h4>{product.name}</h4>
<button onClick={() => onAddToCart(product.id)}>Add to Cart</button>
</div>
);
});

export default function ProductList({ products }: any) {
const handleAddToCart = useCallback((id: string) => {
console.log('Add to cart:', id);
}, []); // 🧠 useCallback prevents new function creation on every render

return (
<div>
{products.map((p: any) => (
<ProductItem key={p.id} product={p} onAddToCart={handleAddToCart} />
))}
</div>
);
}

//In the class version, ProductItem can't easily be memoized with React.memo and useCallback, so it may re-render unnecessarily.
class ProductList extends React.Component {
  handleAddToCart = (id) => {
    console.log('Add to cart:', id);
  };

  render() {
    return (
      <div>
        {this.props.products.map((product) => (
          <ProductItem
            key={product.id}
            product={product}
            onAddToCart={this.handleAddToCart}
          />
        ))}
      </div>
    );
  }
}

12. Inline functions can cause memory leaks issues and failing in comparing props.

Using inline functions in React (e.g., directly inside JSX) can hurt performance because a new function is created every time the component re-renders. This breaks referential equality checks used by React.memo causing child components to re-render unnecessarily—even if their props haven’t changed.

```js
//SHOULD NOT
default class CommentList extends React.Component {
state = {
comments: [],
selectedCommentId: null
}

render(){
const { comments } = this.state;
return (
comments.map((comment)=>{
return <Comment onClick={(e)=>{
this.setState({selectedCommentId:comment.commentId})
}} comment={comment} key={comment.id}/>
})
)
}
}
```

//Change to arrow function:

```js
default class CommentList extends React.Component {
state = {
comments: [],
selectedCommentId: null
}

onCommentClick = (commentId)=>{
this.setState({selectedCommentId:commentId})
}

render(){
const { comments } = this.state;
return (
comments.map((comment)=>{
return <Comment onClick={this.onCommentClick}
comment={comment} key={comment.id}/>
})
)
}
}
```

13. Avoid using Index as Key for map

It can cause issues with display wrong data as React consider components with same key if they are removed and added to a list. When we working with a list dynamically, we need to use unique key for item components from the list. Such as using item id if it has, or using a short_id to create a unique key.

```js
import shortid from "shortid";
{
comments.map((comment, index) => {
<Comment
{..comment}
key={shortid.generate()} />
})
}
```

or

```js
//comment.id can be a uuid.
{
comments.map((comment, index) => {
<Comment
{..comment}
key={comment.id} />
})
}
```

14. Avoid using props in initial state.

When we use props to init state, if the props change it will not update state. If you copy props into useState, your component keeps two versions of the same data — one from props and one in state. This uses more memory and can cause the UI to show old or incorrect data if you’re not careful to sync them.

//SHOULD NOT : Copy props to state
function ProductCard(props: { price: number }) {
  const [price, setPrice] = useState(props.price); // Bad: copies prop to state

  // If props.price changes, `price` won't update unless we add useEffect

  return <div>Price: {price}</div>;
}

//SHOULD : Use props directly
function ProductCard(props: { price: number }) {
  return <div>Price: {props.price}</div>; // Simple and efficient
}




15. Avoid Spreading props on DOM elements

Avoid spreading props directly onto DOM elements because it can introduce invalid HTML attributes, unexpected behavior, and unnecessary re-renders. Instead, explicitly pass only the props you intend to use. This keeps components cleaner, avoids React warnings, and improves rendering performance by reducing reconciliation overhead.

```js
//SHOULD NOT
const CommentsText = props => {
return (
<div {...props}>
{props.text}
</div>
);
};
```

```js
//SHOULD
const CommentsText = props => {
return (
<div specificAttr={props.specificAttr}>
{props.text}
</div>
);
};
```

16. Avoid using async and update state in componentWillMount

Avoid using async logic and updating state inside componentWillMount because it can lead to race conditions, memory leaks, and multiple unintended renders — especially in concurrent rendering. ComponentWillMount will be called before rendering, so that we don’t have access to ref of DOM tree, Instead, move such logic into componentDidMount or useEffect, which run after the initial render, providing safer and more performant behavior aligned with modern React best practices.


```js
//USE
function componentDidMount() {
axios.get(`api/comments`)
.then((result) => {
const comments = result.data
this.setState({
comments: comments
})
})
}
```

17. Throttling and Debouncing Event Action in JavaScript

Throttling means that we will deplay few miliseconds before event trigger to avoid it is called many times. Such as fetch data when scrolling to bottom.
Debouncing also is a technique to prevent actions from triggered many times. We can use debouncing function from lodash library.

```js
import debouce from 'lodash.debounce';

class SearchComments extends React.Component {
constructor(props) {
super(props);
this.state = { searchQuery: “” };
}

setSearchQuery = debounce(e => {
this.setState({ searchQuery: e.target.value });

// Fire API call or Comments manipulation on client end side
}, 1000);

render() {
return (
<div>
<h1>Search Comments</h1>
<input type="text" onChange={this.setSearchQuery} />
</div>
);
}
}
```

18. UseTransition

startTransition in useTransition helps improve performance by marking some state updates as low-priority. This lets React keep the UI responsive during heavy updates, like loading large lists or switching views. Without it, all updates are treated equally and may block the UI. By using startTransition, you allow React to prioritize user interactions while deferring less urgent updates in the background, leading to smoother experiences.

```js
import React, { useState, useTransition } from 'react';

function MyComponent() {
const [state, setState] = useState(initialState);
const [isPending, startTransition] = useTransition();

function handleClick() {
startTransition(() => {
setState(newState); // This state update is marked as a transition
});
}

return (
<>
{/* Your component JSX */}
<button onClick={handleClick}>Update State</button>
{isPending && <div>Loading...</div>}
</>
);
}
```

19. CSS Animations Instead of JS Animations.

Using CSS animations instead of JavaScript animations improves performance because CSS animations are handled by the browser’s compositor thread, allowing them to run smoothly without blocking the main JavaScript thread.

//SHOULD NOT : JS animation, causes main thread work and can lead to dropped frames
function moveBox() {
const box = document.getElementById('box');
let pos = 0;
const interval = setInterval(() => {
if (pos >= 300) clearInterval(interval);
pos += 5;
box.style.transform = `translateX(${pos}px)`;
}, 16); // ~60fps
}
//SHOULD : CSS in html
<style>
  .box {
    transition: transform 1s ease-in-out;
  }
  .move {
    transform: translateX(300px);
  }
</style>

<div id="box" class="box"></div>
<button onclick="document.getElementById('box').classList.add('move')">Move</button>

20. Analyzing and Optimizing Your Webpack Bundle Bloat

We need to optimize the Webpack configuration to reduce bundle size, improve load times, and enhance overall performance of the web application. By analyzing the bundle and applying best practices—such as using SplitChunksPlugin to separate vendor and app code, enabling tree-shaking by setting sideEffects: false, using TerserPlugin for minification, and lazy-loading dynamic imports—we ensure users download only what they need. This results in faster initial page loads, improved user experience, and lower bandwidth usage, especially critical for mobile users or those with slower networks. Proper Webpack optimization is essential in production-ready applications to meet performance standards

// Example of webpack config
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');

module.exports = {
mode: 'production', // ✅ Enables all built-in production optimizations (minification, tree-shaking, etc.)

entry: './src/index.tsx', // ✅ Single entry point for the app

output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js', // ✅ Uses contenthash for long-term caching
assetModuleFilename: 'assets/[name].[contenthash][ext]', // ✅ Optimized static asset caching
publicPath: '/', // ⚠️ Adjust based on your hosting setup (e.g., '/' for SPA routing)
},

resolve: {
extensions: ['.tsx', '.ts', '.js'], // ✅ Allows imports without file extensions
},

module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader', // ✅ TypeScript transpilation
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader, // ✅ Extracts CSS into separate files for better caching
'css-loader',
],
},
{
test: /\.(png|jpg|jpeg|gif|svg|woff2?|ttf|eot)$/,
type: 'asset', // ✅ Automatically selects best handling for assets (inline or separate file)
},
],
},

optimization: {
minimize: true,
minimizer: [new TerserPlugin()], // ✅ Minifies JS using Terser
splitChunks: {
chunks: 'all', // ✅ Code splitting for vendor and common chunks
},
runtimeChunk: 'single', // ✅ Extracts runtime into its own file for better caching
},

plugins: [
new CleanWebpackPlugin(), // ✅ Cleans output dir before build to prevent leftover files

new HtmlWebpackPlugin({
template: './public/index.html',
minify: {
collapseWhitespace: true,
removeComments: true, // ✅ Reduces HTML size
},
}),

new MiniCssExtractPlugin({
filename: '[name].[contenthash].css', // ✅ Long-term caching for CSS
}),

new CompressionPlugin({
algorithm: 'gzip', // ✅ Compresses files to reduce network payload
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
}),

// ✅ Define environment for app & libraries (like React production mode)
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
};

Picture of Quang Truong

Quang Truong

Line Manager at NashTech, I am a curious and motivated software engineer with a passion for creating applications that make life easier and more enjoyable.

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top