GrapeCity
Published in

GrapeCity

How to Optimize and Create the Fastest React Datagrid Application

Optimization is a crucial part of the design process when building internet applications. According to studies, the ideal time for a website to load a page is 2–3 seconds, any longer than that. The user’s chance to leave the page before it loads increases dramatically, and pages that load faster see increased ad revenue from their userbase.

This is even more crucial when visiting pages on a mobile device, which see an even higher bounce rate than pages loaded on a desktop as the time to load increases. With the increase in users visiting sites on mobile devices, Google has since switched to favoring mobile-first indexing.

This means that optimization is even more crucial, with Google favoring indexing websites based on Googlebot’s smartphone agent moving forward.

Today’s larger applications deal with large data sets and bundles and perform complex calculations. Thankfully, React and Wijmo’s FlexGrid gives you options to optimize your React DataGrid application. In this blog, we’ll cover the following topics:

  • Unsubscribing From Observables
  • Prevent Re-rendering by Keeping Component State Local
  • Keep Bundle Sizes Small by Code-Splitting
  • Reducing DOM Elements With FlexGrid Virtualization
  • Memoize React Components

Try Wijmo’s controls and download the latest version of Wijmo.

Unsubscribing From Observables

Observables do exactly what they say; they’re things you wish to observe and take action on. They offer an easy way for developers to pass data both to and around their applications. In React, they’re used for event handling, asynchronous programming, and managing different sets of data. However, improper use of observables can lead to a decrease in performance and serious memory management issues.

When using observables, they use a subscribe method to get the data and watch to see if there are any changes with the data, which is all done asynchronously. Typically, you subscribe to a method within a service that makes an HTTP call to get your data, and in most cases, this is done within the componentDidMount() method of your component:

Mock Component

import React, { Component } from 'react';  
import { BrowserRouter as Router, Route } from 'react-router-dom';

import { mockService } from '@/services';

class Mock extends React.Component {
constructor(props) {
super(props);
this.state = {
mockData: []
};
}

componentDidMount() {
this.mockSubscription = mockService.getAPIData().subscribe(data => {
this.setState({ mockData: data });
}
}

render() { ... }
}

With that complete, we can now asynchronously access our data from the API and receive any changes made to the data through our subscription. However, we’re missing one crucial detail: the unsubscribe() method. With our component set up like it is, the subscription connection will remain open if we navigate away from it. This can lead to memory leaks, causing severe performance issues.

Since we want this connection to remain open while the component is loaded, we’ll wait until the component gets removed from the DOM to unsubscribe. React gives us a lifecycle hook that will allow us to do this, called componentWillUnmount():

import React, { Component } from 'react';  
import { BrowserRouter as Router, Route } from 'react-router-dom';

import { mockService } from './services';

class Mock extends React.Component {
constructor(props) {
super(props);
this.state = {
mockData: []
};
}

componentDidMount() {
this.mockSubscription = mockService.getAPIData().subscribe(data => {
this.setState({ mockData: data });
}
}

componentWillUnmount {
this.mockSubscription.unsubscribe();
}

render() { ... }
}

Now, when our users navigate away from this component, our application will unsubscribe from this observable, closing the connection and preventing any memory leaks. If you’d like to learn more about observables, RxJS has documentation that can provide you with more information.

Preventing Re-rendering by Keeping Component State Local

States allow you to store data within your component that can be accessed across the component. When the state gets modified, not only does the component get rerendered, but its child components also get rerendered.

We can prevent these large-scale rerenders by breaking up our components, keeping the data local to children components, and preventing more components from being rendered when our state changes.

Parent Component

import React, { Component } from 'react';  
import { ChildComponent } from './childcomponent';

class ParentComponent extends React.Component {
constructor(props) {
this.state = {
count: 0;
}
}
incrementCount(event) {
this.setState({ count: ++this.state.counter });
}

render() {
return <div>
<div>Count: </div>
<button onClick={this.incrementCount.bind(this)}>Increment Count</button>
<ChildComponent />
</div>
}
}

Child Component

import React, { Component } from 'react';  
import { ChildComponent } from './childcomponent';

class ChildComponent extends React.Component {
constructor(props) {
this.state = {}
}

render() {
return <div><p>This is our Child Component</p></div>
}
}

Now, whenever we click on our button, our count will increment up by 1, but both our ParentComponent and our ChildComponent will rerender. This may not seem like a lot in this scenario since our components are relatively small, but as components become larger, rerendering each one will cost us when it comes to our application’s performance.

We can prevent this rerendering of all of our components by creating a new component, which we’ll call ButtonIncrement, and move the functionality of our button into that component:

Parent Component

import React, { Component } from 'react';  
import { ChildComponent } from './childcomponent';
import { ButtonIncrement } from './buttonincrement';

class ParentComponent extends React.Component {
constructor(props) {
this.state = {}
}

render() {
return <div>
<ButtonIncrement />
<ChildComponent />
</div>
}
}

ButtonIncrement Component

import React, { Component } from 'react';

class ButtonIncrement extends React.Component {
constructor(props) {
this.state = {
count: 0;
}
}
incrementCount(event) {
this.setState({ count: ++this.state.counter });
}

render() {
return <div>
<div>Count: </div>
<button onClick={this.incrementCount.bind(this)}>Increment Count</button>
</div>
}
}

Now that we’ve broken out the button and increment portion of our parent component into its component, we will no longer rerender both the Parent and Child components when the button is clicked; instead, we’ll only be rerendering the ButtonIncrement component. This will save the DOM from any excess rerendering.

Keep Bundle Sizes Small using Code-Splitting

By default, when a React application is loaded into a browser, a bundle file, containing all of the application’s code, is loaded and served to a user all at once. React generates this bundle by merging all of the files that we’ve written for our application that are required for the application to run.

This is done to reduce the number of HTTP requests that a page has to make, increasing the speed at which the page loads. However, as applications get bigger, the bundle file size will also increase, and there will be times when we’re loading content into our bundle file that the user won’t need on certain pages.

This will cause the page’s load speed to decrease, and we’ll be loading unnecessary code into the browser because it will pertain to sections of our application that the user isn’t currently using.

React gives us a way to break up our bundle file into multiple smaller bundle sizes through a process called code-splitting. Code-splitting is done by using the dynamic import() method, followed by lazy-loading our components on-demand by using React.lazy. This dramatically improves the performance when loading large and complex React applications. For example, let’s say we have an import statement for a child component:

import ChildComponent from './childcomponent';</td>

This will automatically include the ChildComponent in our main bundle, which will get loaded when the browser renders our application. However, we want to split this ChildComponent out into its own bundle and load that bundle when the component gets called. Our new import statement would look as follows:

const ChildComponent = React.lazy(() => import('./childcomponent'));</td>

React.lazy takes a function that uses a dynamic import(). This call returns a Promise which resolves to a module with a default export containing our React component. Now, when we want to render this component, it should be rendered inside a Suspense component. This will allow us to show some fallback content (such as a loading icon or a progress bar) while we’re waiting for our lazy component to load:

import React, { Suspense } from 'react';  
const ChildComponent = React.lazy(() => import('./childcomponent'));

function ParentComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}
<ChildComponent />
</Suspense>
</div>
)
}

While we wait for our ChildComponent to load into the browser, we’ll display the text “Loading…”.

If you’d like to learn more about code-splitting, React has plenty of documentation on it, which you can find here.

Reducing DOM Elements With FlexGrid Virtualization

The primary purpose of FlexGrid is to convert JavaScript objects into DOM elements that the user can interact with. In many instances, this data consists of hundreds, thousands, or even millions of rows of data; creating DOM elements for each of these items can be highly resource-heavy, causing slow and bloated pages.

Virtualization is the process of keeping track of which portions of the data are visible to the user and only rendering those sections in the DOM. This greatly reduces the number of DOM elements in the document tree and drastically improves performance, especially when working with very large data sets.

Wijmo exposes the visible part of the data through its viewRange property; whenever the user resizes the screen or scrolls the grid, the viewRange gets updated, which in turn updates the DOM.

To prevent the number of elements in the DOM from ballooning, FlexGrid takes the cells that scroll out of the viewRange and recycles them, removing from the data that they were storing and repopulating them with the new data that is coming into the viewRange. This keeps your DOM lean and your application fast and lightweight.

We can see this demonstrated in this sample:

As you can see, there are currently 100 rows of data currently in the grid, and only 60 cell elements being rendered by the DOM. We get this number by using the following code:

flexGrid.updatedView.addHandler((s, e) => {  
this.setState({
rowCount: s.rows.length.toString(),
cellCount: s.hostElement.querySelectorAll(".wj-cell").length.toString()
});
});</td>

The s.hostElement.querySelectorAll(‘.wj-cell’) method returns the an array of elements rendered in the DOM that have the .wj-cell class appended to them. As we scroll down the grid, we see that the number of rows of data within FlexGrid increase, but the number of cell elements stay the same:

Memoize React Components

In a previous section of this article, we discussed refactoring code to prevent our ParentComponent from rerendering both itself as well as its ChildComponent. Now, we’ll talk about using memoization to improve our application’s performance.

Memoization is an optimization strategy that allows us to cache a component, saving the result in memory, and returning the cached result for the same input. Essentially, when a child component receives props, a memoized component will do a shallow comparison of the props by default, and skip re-rendering the child component if its props haven’t changed. This is done by using React.memo. Let’s use the following component as an example:

export function Car({ brand, model, year }) {  
return (
<div>
<div>Car Brand: { brand }</div>
<div>Car Model: { model }</div>
<div>Year Released: { year }</div>
</div>
);
}

export const MemoizedCar = React.memo(Car);

We’re now returning a component, MemoizedCar, which provides the exact same contents as our Car component, with one difference — its render is memoized. Now, React will reuse our memoized content as long as our model and year props are the same between renderings. For example, take the following code:

<MemoizedCar brand="Toyota" model="Camry" year="2022" />

The first time this code is run, our application will go through the process of rendering the MemoizedCar component. React will do its shallow comparison of the props provided to the component; if they have not changed, React will skip over this component when it re-renders the DOM. This boosts our performance by reusing content that we’ve already rendered. You want to take a few things into account when choosing whether or not your application memoizes a component. When using React.memo, you want to be sure that:

  • Your component is functional and given the same props, always renders the same output
  • Your component renders often
  • Your component is (usually) provided the same props during rerendering
  • Your component contains a reasonable amount of UI elements to reason a props equality check

With all of these tools at your disposal, you’ll now be able to reduce the amount of work that React has to perform when users interact with your application, as well as lighten the load of the browser’s DOM when it comes to rendering and destroying components.

Happy coding!

Originally published at https://www.grapecity.com on May 6, 2022.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store