10 Expert Performance Tips Every Senior JS React Developer Should Know

Prashant
10 min readJul 9, 2023

--

As a senior Javascript React developer, consistently improving the performance of your applications is an essential skill to master. We’ve gathered the top 10 expert performance tips that will elevate your React development game.

Let’s take a deep dive into these advanced techniques, illustrated with code examples, and supercharge your React skills!

Efficient Component Rendering

Efficient component rendering is fundamental for top-notch React applications. In this section, we’ll cover various strategies to optimize render performance and help ensure lightning-fast page loads.

React.memo and PureComponents

React.memo and PureComponent are powerful techniques to prevent unnecessary component re-renders. React.memo is a higher-order component for functional components, while PureComponent is a base class for class components. Both optimize the rendering process by only allowing re-renders when the component’s props have shallowly changed.

Let’s compare a regular component with one using React.memo:

// Regular functional component
const TitleRegular = ({ text }) => {
console.log("Rendering Regular Title...");
return <h2>{text}</h2>;
};
// Memoized functional component using React.memo
const TitleMemoized = React.memo(({ text }) => {
console.log("Rendering Memoized Title...");
return <h2>{text}</h2>;
});

By wrapping our functional component with React.memo, we prevent excessive re-renders and boost performance. However, use these techniques wisely and only when necessary, as they can be counterproductive if applied indiscriminately.

Using shouldComponentUpdate

shouldComponentUpdate is a lifecycle method exclusive to class components. It’s a boolean function that allows you to decide when a component should re-render based on changes in the props or state. By implementing an optimized shouldComponentUpdate, you avoid unnecessary re-rendering and drastically improve your application’s performance. Here’s an example:

class UserProfile extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// Only update if the user's ID has changed
return nextProps.user.id !== this.props.user.id;
}
render() {
const { user } = this.props;
return <div>{user.name}'s Profile</div>;
}
}

In this example, the UserProfile component only re-renders when the user’s ID has changed. Using shouldComponentUpdate effectively provides a huge performance boost, especially when dealing with complex components.

Leveraging useCallback and useMemo

useCallback and useMemo hooks are effective solutions to prevent unnecessary re-renders and redundant re-computation of values in functional components. useCallback memoizes a callback function, so it’s not re-created on every render, while useMemo memoizes computed values.

Here’s an example on how to optimize a component with useCallback:

import React, { useState, useCallback } from "react";
const Parent = () => {
const [counter, setCounter] = useState(0);
const incrementCounter = useCallback(() => setCounter(counter + 1), [counter]); return (
<>
<Child onClick={incrementCounter} />
Counter: {counter}
</>
);
};

By using useCallback, we ensure that incrementCounter is only re-created when the counter state changes, optimizing the child component’s rendering.

Mastering React State Management

Your React app’s performance is highly dependent on how you handle state management. Let’s dive into three essential aspects of state management: immutability, global state management with the Context API, and code-splitting with useState and useReducer.

Immutability and State Updates

Immutability means not mutating data directly, but creating new references with updated values. It’s a crucial concept in React, as it helps maintain consistent state and enables better optimization with PureComponent and React.memo.

Here’s an example of updating state immutably:

const [items, setItems] = useState([]);
const addItem = (newItem) => {
// Create a new array with the newItem and existing items (immutably)
setItems((prevItems) => [...prevItems, newItem]);
};

By updating the state immutably, we ensure that our application’s performance remains optimal and prevent hard-to-find bugs.

Context API for Global State Management

The Context API is an excellent solution for managing global state in your React application without introducing additional libraries like Redux. Context allows you to share values among components without passing props down the component tree.

Here’s a simple example of using the Context API:

import React, { createContext, useState, useContext } from "react";
const ThemeContext = createContext();const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};const useTheme = () => useContext(ThemeContext);const App = () => (
<ThemeProvider>
{/* Your app's components */}
</ThemeProvider>
);// Inside a component, it's as simple as:
const { theme, setTheme } = useTheme();

By integrating the Context API within your state management strategies, you’ll achieve better performance and modularize your app efficiently.

Using Code-Splitting with useState and useReducer

Code-splitting is a technique that allows you to break up your application into smaller “chunks” which are loaded on-demand. This strategy results in faster initial load times and improved overall performance.

You can achieve code-splitting with React.lazy and React.Suspense. However, it’s also possible to use code-splitting with useState and useReducer to optimize your app’s state management further:

import React, { useState, useEffect } from "react";
// useAsyncFn is a custom hook that handles code-splitting for your state management functions
import useAsyncFn from "./useAsyncFn";const TodoList = () => {
const [todos, setTodos] = useState([]);
// Load the importFn lazily (code-splitting)
const [importFn, { loading }] = useAsyncFn(async () => {
const { setTodosWithCodeSplitting } = await import("./setTodos");
return setTodosWithCodeSplitting;
}); useEffect(() => {
importFn().then((newTodos) => setTodos(newTodos));
}, [importFn]); // Render your component
};

By applying code-splitting to your state management strategies, you ensure a performant and scalable application that is a delight to use.

Optimizing Component Reusability

Component reusability is one of the primary strengths of React. In this section, we’ll explore three techniques for optimizing component reusability: higher-order components, creating effective custom hooks, and encapsulating styles with styled-components.

Developing Higher-Order Components

Higher-order components (HOCs) are functions that take a component and returns a new component with additional props or behaviors. Developing reusable HOCs can significantly optimize your application’s architecture, reducing redundancy and complexity.

Here’s an example of a HOC that injects a user prop to components:

const withUser = (Component) => {
return class extends React.Component {
// Your additional logic here
render() {
const user = { name: "John Doe", id: 1 };
return <Component {...this.props} user={user} />;
}
};
};
const UserProfile = ({ user }) => (
<div>{user.name}'s Profile</div>
);const UserProfileWithUser = withUser(UserProfile);

Developing modular and reusable HOCs helps create clean and maintainable codebases while optimizing component reusability.

Creating Effective Custom Hooks

Custom hooks are a powerful tool for reducing redundancy, sharing logic across components, and maintaining clean code. Learning to create effective custom hooks is essential for optimizing component reusability and improving your app’s performance.

Here’s an example of a custom hook for fetching data:

import { useState, useEffect } from "react";
const useFetchData = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true); useEffect(() => {
const fetchData = async () => {
setLoading(true);
const response = await fetch(url);
const data = await response.json();
setData(data);
setLoading(false);
};
fetchData();
}, [url]); return { data, loading };
};// Usage: const { data, loading } = useFetchData("https://api.example.com/data");

By mastering custom hooks, you’ll be able to create modular logic, optimize component reusability, and write efficient, maintainable code.

Encapsulating Styles with Styled Components

Styled-components is a popular CSS-in-JS library that allows you to scope your styles to individual components. By encapsulating styles within each component, you avoid conflicts, enhance maintainability, and optimize reusability.

Here’s an example of styled-components:

import styled from "styled-components";
const MyButton = styled.button`
background-color: ${({ primary }) => (primary ? "blue" : "white")};
color: ${({ primary }) => (primary ? "white" : "black")};
`;const App = () => (
<>
<MyButton primary>Primary Button</MyButton>
<MyButton>Secondary Button</MyButton>
</>
);

By mastering styled-components, you’ll be able to create clean, modular, and maintainable styles that boost component reusability.

Mastering React Router Performance

React Router is one of the most popular libraries for navigating SPA (Single Page Applications) in React. Ensuring top performance in your React Router implementation can result in a more seamless user experience. In this section, we’ll cover code-splitting with React.lazy and Suspense, preloading techniques, and utilizing nested routes efficiently.

Code-Splitting with Lazy and Suspense

Using React.lazy() and <React.Suspense> allows you to seamlessly perform code-splitting in your React Router implementation. This results in faster initial page loads and on-demand loading for navigated pages.

Here’s an example of applying code-splitting with React.lazy and Suspense:

import React, { lazy, Suspense } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);

By using React.lazy(), the Home and About components are loaded on-demand, ensuring a more performant browsing experience.

Implementing Preloading Techniques

Preloading allows you to load specific content or components in advance, reducing wait times for users. This technique can significantly enhance the user experience, especially when applied strategically.

Here’s an example of preloading components as users hover navigation links:

import React from "react";
import { NavLink } from "react-router-dom";
const preloadComponent = (importPromise) => {
importPromise.catch((err) => console.error("Failed to preload:", err));
};const App = () => (
<nav>
<NavLink
to="/home"
onMouseOver={() => preloadComponent(import("./Home"))}
>
Home
</NavLink>
<NavLink
to="/about"
onMouseOver={() => preloadComponent(import("./About"))}
>
About
</NavLink>
</nav>
);

In this example, hovering over the NavLink components preloads the Home and About components, making navigation faster and smoother.

Using Nested Routes Efficiently

When dealing with complex applications, properly structuring and utilizing nested routes can make your code more maintainable and your app more performant.

Consider the following example:

import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
const App = () => (
<Router>
<nav>
<ul>
<li>
<Link to="/users/john">John</Link>
</li>
<li>
<Link to="/users/jane">Jane</Link>
</li>
</ul>
</nav> <Route
path="/users/:username"
render={({ match }) => <UserPage username={match.params.username} />}
/>
</Router>
);const UserPage = ({ username }) => (
<>
<h1>{username}'s Page</h1>
{/* Content here */}
</>
);

In this example, the UserPage component efficiently handles nested routes, keeping the codebase clean, organized, and performant.

Advanced React Performance Tools

Optimizing React performance requires insight and analysis. In this section, we’ll cover profiling components with React Developer Tools, implementing error boundaries, and leveraging Web Workers for complex computations.

Profiling Components with React Developer Tools

React Developer Tools is an essential browser extension that can significantly improve your debugging and optimization process. Profiling components within your application can help you identify performance bottlenecks, unnecessary re-renders, and more.

Here’s an example of how to use React Developer Tools:

  1. Install the React Developer Tools extension in your browser.
  2. Open React Developer Tools from the browser’s developer tools panel.
  3. Select the Profiler tab.
  4. Press the Record button and interact with your application.
  5. Stop recording, and analyze the data presented in the Profiler for potential optimization opportunities.

Implementing Error Boundaries

Error boundaries are a powerful React feature that helps you catch and handle errors gracefully. By implementing error boundaries in your application, you can prevent crashes and unexpected behaviors that may affect performance.

Here’s an example of an error boundary component:

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
} componentDidCatch(error, errorInfo) {
// Log the error to an error reporting service
console.error("Caught an error:", error, errorInfo);
} render() {
if (this.state.hasError) {
// You can render a custom fallback UI
return <h1>Something went wrong.</h1>;
} return this.props.children;
}
}// Usage: Wrap your components with ErrorBoundary
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>

Leveraging Web Workers for Complex Computations

Web Workers allow you to run background tasks without affecting your application’s UI performance. For computationally intensive operations, using Web Workers can significantly improve performance and responsiveness.

Here’s a simple example of using a Web Worker:

// worker.js
self.onmessage = (event) => {
const result = expensiveCalculation(event.data);
self.postMessage(result);
};
// In your component
const runCalculation = async (data) => {
const worker = new Worker("./worker.js");
worker.postMessage(data); worker.onmessage = (event) => {
const result = event.data;
console.log("Result from the worker:", result);
}; worker.onerror = (error) => {
console.error("Error from the worker:", error);
};
};

By offloading expensive calculations to Web Workers, you keep your app performant and responsive without compromising functionality.

DOM Optimizations for Large UIs

When working with large and complex user interfaces, optimizing DOM interactions is crucial. In this section, we’ll discuss virtualization, debouncing and throttling event handlers, and customizing event delegation.

Virtualization with React-window or React-virtualized

Virtualization is a technique that allows you to render only a subset of items in a large list, improving UI performance for sizable datasets. Libraries like react-window and react-virtualized are excellent choices for implementing virtualization in your React application.

Here’s an example of using react-window for virtualization:

import { FixedSizeList } from "react-window";
const Row = ({ index, style }) => {
// Render a single row
return <div key={index} style={style}>Row {index}</div>;
};const VirtualizedList = ({ items }) => (
<FixedSizeList
height={300}
width={500}
itemCount={items.length}
itemSize={30}
>
{Row}
</FixedSizeList>
);

By using virtualization libraries like react-window, you can significantly improve performance when dealing with large lists and data-heavy interfaces.

Debouncing and Throttling Event Handlers

Debouncing and throttling are techniques that help reduce the number of function calls in response to user interactions or other events. By applying these techniques to event handling, you can improve your application’s performance, particularly during continuous user interactions like scrolling or typing.

Here’s an example of a debounce function:

const debounce = (fn, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout); timeout = setTimeout(() => {
fn(...args);
}, wait);
};
};// Usage:
const debouncedFunction = debounce(myFunction, 300);

Using debounce and throttle wisely can reduce the strain on your application during continuous user interactions, improving overall performance.

Customizing Event Delegation in React

Event delegation is a technique where a single event listener is attached to a parent element instead of multiple child elements. React uses event delegation by default, but customizing event delegation can help you optimize your application’s performance.

Consider the following example:

class CustomEventHandler extends React.Component {
handleItemClick(event) {
if (!event.target.matches(".item")) return;
console.log("Clicked item:", event.target.textContent);
}
render() {
return (
<ul onClick={this.handleItemClick}>
<li className="item">Item 1</li>
<li className="item">Item 2</li>
<li className="item">Item 3</li>
</ul>
);
}
}

In this example, instead of attaching three separate click listeners to each list item, a single event handler on the parent <ul> element is used to handle item clicks efficiently.

Server-Side Rendering and Static Generation

Server-side rendering (SSR) and static site generation (SSG) are essential techniques in creating high-performing, SEO-friendly React applications. In this section, we’ll explore using Next.js for SSR, Gatsby for SSG, and incremental static regeneration.

SSR with Next.js for SEO and Performance

Next.js is a popular framework for building high-performance, server-rendered React applications. With Next.js, you can achieve faster initial page loads, better SEO, and overall improved performance.

To create a Next.js application, follow these steps:

  1. Install Next.js and its dependencies: npm install next react react-dom.
  2. Add the following scripts to your package.json:
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
  1. Create a pages directory and add your page components.

Next.js automatically handles server-side rendering, code-splitting, and more, ensuring a high-performance React application.

SSG with Gatsby in large-scale React Applications

Gatsby is a powerful static site generator for React that is ideal for large-scale applications, especially when incorporating content from various sources. Gatsby provides a highly performant, SEO-fr

--

--