Optimization Techniques and Best Practices for React Application

·

8 min read

Featured on Hashnode

Scaling a React application is a task in itself. We developers need to look into multiple corners of the application for optimizations to improve performance and provide a great user experience. This article helps you with major optimization techniques and code practices that you can incorporate into your React application.

Bifurcate business logic from UI component

Keeping React components clean with all the business logic in a separate file can help to improve the code quality, maintainability, and readability. Let’s see an example of an email submission to understand how this can be achieved.

import React, { useState } from "react";

// Example to show the submitted email by the user
const EmailSubmission = () => {
  const [emailId, setEmailId] = useState("");
  const [enableView, setEnableView] = useState(false);

  return (
    <>
      <input
        type="text"
        value={emailId}
        placeholder="Enter Email Id"
        onChange={(evt) => {
          let value = evt.target.value;
          setEmailId(value);
        }}
      />
      <button
        onClick={() => {
          if(emailId){
            setEnableView(true);
            // more code to send data to server via API
          }
        }}
      >
        Submit Email
      </button>
      {enableView && <h3>Entered Email Id - {emailId}</h3>}
    </>
  );
};

export default EmailSubmission;

In the above example, you can see both the UI elements and the business logic to submit and display the entered user email, resides in a single component. This can be a very small example, but, when building a large component with multiple business logic in place, separating both will help in a better understanding of code and scalability.

Now let’s dive into how we can separate business logic from the UI component.

import React, { useState } from "react";
import { submitEmail } from "utils.js";
// Example to show the submitted email by the user
const EmailSubmission = () => {
  const [emailId, setEmailId] = useState("");
  const [enableView, setEnableView] = useState(false);

  const handleUserInput = (event) => {
    let email = event.target.value;
    setEmailId(email);
  };

  return (
    <>
      <input
        type="text"
        value={emailId}
        placeholder="Enter Email Id"
        onChange={handleUserInput}
      />
      <button
        onClick={() => {
          submitEmail(emailId);
        }}
      >
        Submit Email
      </button>
      {enableView && <h3>Entered Email Id - {emailId}</h3>}
    </>
  );
};

export default EmailSubmission;
export const submitEmail = (emailId) => {
  if (emailId) {
    // do more...
    // more code to send the email-id to the server via API
  }
};

Here we have separated UI components and business logic which gives clear visibility for understanding.

Break down heavy components into multiple sub-components As the application grows, multiple UI elements get added to the component, and eventually, the code becomes lengthy and clumsy. To maintain better code readability, there is a need to break down the parent component into one or more sub-component(children component). This will also, improve the reusability of components and maintain consistency.

Let’s take the previous example to break down the component into multiple sub-components namely — Button and Input

import React from "react";
import PropTypes from "prop-types";

const Input = (props) => {
  const { type, value, name, handleChange, placeholder } = props;

  return (
    <input
      type={type}
      name={name}
      value={value}
      onChange={handleChange}
      placeholder={placeholder}
    />
  );
};

Input.defaultProps = {
  type: "text",
  name: "input",
  value: "",
  handleChange: () => null,
  placeholder: "",
};

Input.propTypes = {
  type: PropTypes.string.isRequired,
  name: PropTypes.string,
  value: PropTypes.string.isRequired,
  handleChange: PropTypes.func.isRequired,
  placeholder: PropTypes.string,
};

export default Input;
import React from "react";
import PropTypes from "prop-types";

const Button = (props) => {
  const { onClick, btnText } = props;
  return <button onClick={onClick}>{btnText}</button>;
};

Button.defaultProps = {
  btnText: "Submit",
  onClick: () => null,
};

Button.propTypes = {
  btnText: PropTypes.string,
  onClick: PropTypes.func,
};

export default Button;

Since we have created two new components, They can be reused throughout the application without code duplication.

Avoid multiple re-rendering

React re-renders the entire component when there is a change in state or props. React re-renders the children components too, when there is a change in the component state. If you think of avoiding prop drilling, the tricky thing is, even if a child component has no props passed to it from the parent component, React will still re-render the child component just because the parent re-rendered!

Well, how can we reduce or avoid multiple re-rendering? The answer is ‘React.memo’. React.memo is a Higher Order Component(HOC) that prevents a component from re-rendering if the props (or values within it) have not changed.

Here is an example of a React component that accepts props and displays vehicle details accordingly using React.memo, which re-renders only when the props change.

const VehicleDetails = ({ vehicle }) => {
  const { licensePlate, vehicleColor, vehicleModel, vehicleMake } = vehicle;
  return (
    <>
      <h4>{licensePlate}</h4>
      <p>{vehicleColor}</p>
      ...
    </>
  );
};

export default React.memo(VehicleDetails);

Speed up your application loading by code splitting and compression

While building a large-scale React application, the build size increases as you start implementing new features and adding dependencies. Sending this huge JavaScript bundle file to the client’s browser contributes to the slower load time of the application. To overcome this, we can use a technique called ‘Code splitting’.

Code splitting can be achieved by using any module bundlers. These module bundlers help in separating or splitting your built application code into multiple ‘chunks’ and serve it to the browser when necessary. Hence, reducing the load time on the browser.

If you are using Webpack for bundling, then you can achieve this kind of code-splitting capability. By splitting the bundled files, web browsers cache less frequently changing files and parallel downloads resources to reduce load time.

Webpack not only helps in code-splitting, But it also provides Tree shaking capabilities, and Dependency optimization using plugins that help in the removal of unused dependencies in your final bundle.

— Check out more about Webpack configurations required for web applications here. — Check the list of dependencies that you can optimize.

Gzip Compression

Gzip compression allows the webserver to provide a smaller file size, which helps the application load faster. The reason gzip works well is that JavaScript, CSS, and HTML files use a lot of repeated text with lots of whitespaces. Since gzip compresses common strings, this can reduce the size of pages and style sheets by up to 70%, shortening the application’s first render time.

If you are using Node.JS/Express backend, you can use Gzipping to compress your bundle size with the compression module.

const express = require('express');
const compression = require('compression');
const app = express();

// Pass `compression` as a middleware!
app.use(compression());

Lazy loading & Dynamic import

Lazy loading is a performance optimization technique for web applications to load or render web components on demand. This technique determines when an element leaves and enters a viewport. This is an efficient way to load the components that appear on the viewport.

Lazy loading images

Implementing lazy loading for images is the usual way to improve the performance of web and mobile applications, where you get the list of images to be rendered. Browser-level support for lazy-loading images is now supported on the web! Here is a code snippet to implement image lazy-loading in the img tag.

<img src="some-image-url.extension" loading="lazy" />

In the above code snippet, you can see a new attribute — loading that helps in image lazy loading.

Also, there are multiple lazy loading libraries(For Images and Components)for React applications that make it easier to speed up the performance of React apps and improve the overall user experience.

Dynamic import of components

ES2020 introduced dynamic imports, where the modules are loaded dynamically through which we can reduce the final bundle needed for the initial page load by importing modules at runtime. This is incredibly powerful because it allows you to only ship the code that needs to run after the document has loaded.

Dynamic import in React application using ‘React.lazy’ We can dynamically import components using React.lazy. This method makes it easy to code-split a React application on a component level using dynamic imports. This is known as component-based splitting. The React.lazy component is a function that takes another function as an argument, which in turn calls a dynamic import and returns a promise. *React.lazy() *handles this promise and expects it to return a module that contains a default export React component.

Let’s see how static and dynamic import code looks.

import Login from "Pages/Login.js";
import ErrorView from "Pages/Error.js";
import SuccessView from "Pages/Success.js";
...
// more react code

The above code represents the static import of Login, Success, Error components.

import React, {lazy} from "react";
import Login from "Pages/Login.js";
const ErrorView = lazy(()=> import("Pages/Error"));
const SuccessView = lazy(()=> import("Pages/Success"));
...
// more react code

The success and error page is now lazy-loaded, ensuring that both chunks are only loaded when it's rendered.

Have some standards in your code — Best Practices

Writing clean code and maintaining a coding standard in your application leads to a better understanding and is easy to scale.

Following are best practices you can include in your application

  1. Use Functional Components with Hooks over Class Components.

  2. Type-check your component’s props by defaultProps and propTypes.

  3. Dead tree shaking — Removal of duplicate, unwanted and dead code from the application code base.

  4. Use ES6 or any latest ECMA coding standards for JavaScript.

  5. Follow best JS code practices

  6. Avoid installing packages/libraries for small features, if not, it will increase the project’s bundle size and in turn, increase application loading time.

Conclusion

With that said, there are multiple ways to optimize React applications. In this article, I have tried to summarize a few major techniques. Fine-tuning with small changes and maintaining code consistency in your application results in better performance, Which can have a positive impact on users and also the developers.