Miłosz Orzeł

.net, js, html, arduino, java... no rants or clickbaits.

ag-Grid (React) Cell Renderers Performance

TL;DR

Vanilla JS cell renderers are faster than framework renderers (just like the docs suggest) but renderers created with React class components are quite decent. Unfortunately, cell renderers build with function components are noticeably worse. The grid team is aware of the issue and might fix it soon (I've done my test on ag-Grid 23.1.0 released just 2 days ago)... See live demo that compares different ways to render cell content and check the source code on GitHub.

 

THE COMPARISON APP

I needed to create some advanced cell renderers to work around limitations of cell editors (to get full control of when row edition stops, to allow multi-row edit and to make it play nicely with Redux)... But before that, I wanted to get and idea of how much slower React-based cell renderers are compared to a plain JS cell rendering function. I wanted to test rudimentary cell renderers to get and idea about the overhead added by using frameworkComponents for cell rendering... 

Click here to see an app that lets you compare cell renderers performance. The app builds a grid with 100 rows and 400 columns but only 100 of these columns are visible at any given moment based on the renderer type choice. Switching renderer type (that is switching columns visibility) or scrolling through the grid gives you a chance to see how renderers perform. There's also a checkbox that lets you assess the impact of disableStaticMarkup option.

The app was done with ag-Grid 23.1.0 and React 16.3.1 (the most recent versions at the time of this writing) and tested in Chrome 81, Firefox 75, Edge 44.

Here's how the test application looks (showing cells rendered with React class component):

Cell renderers test app... Click to enlarge...

 

If you want to run the app on your machine: clone the repo, do npm install and npm start (just like with any other project initiated with create-react-app)...

How does it work?
"Not set" option means that column definition lacks cellRenderer option completely. "Vanilla JS function" will cause the cell to be rendered with such renderer:

export default function vanillaFunctionRenderer(params) {
    return `<span>VF: ${params.value}</span>`;
}

"React class component" means such renderer:

import React, { Component } from 'react';

export default class ReactClassRenderer extends Component {
    render() {        
        return (
            <span>RC: {this.props.value}</span>
        );
    }
}

and this is the renderer for "React function component" option:

import React from 'react';

export default function ReactFunctionRenderer(props) {    
    return (
        <span>RF: {props.value}</span>
    );
}

Please notice that all 3 renderers do the same simple thing: render a span with cell value prefixed with "VF: ", "RC: " or "RF: " to make it easier to see which render is actually used by the columns visible on the gird. Notice also that React state is not used (neither class state nor state hook).

Below you can see how ag-Grid is configured (mind components, frameworkComponents and disableStaticMarkup options) and how column visibility is controlled with calls to column API setColumnsVisible (using bulk visibility change is a lot faster than using individual calls to setColumnVisible).

// Imports skipped

class Grid extends Component {
    constructor(props) {
        super(props);

        const [columnDefs, rowData] = generateColumnsAndRows(100, 100);

        this.state = {
            columnDefs,
            rowData,
            disableStaticMarkup: false
        }
    }

    setColumnsVisiblity = rendererType => {
        const allColumns = this.gridColumnApi.getAllColumns();

        const columnsToHide = allColumns.filter(c => c.colDef.cellRenderer !== rendererType);
        const columnsToShow = allColumns.filter(c => c.colDef.cellRenderer === rendererType);

        this.gridColumnApi.setColumnsVisible(columnsToHide, false);
        this.gridColumnApi.setColumnsVisible(columnsToShow, true);
    }

    handleGridReady = params => {
        this.gridColumnApi = params.columnApi;
    }

    handleRendererTypeChange = event => {
        this.setColumnsVisiblity(event.target.value || undefined);
    }

    handleDisableStaticMarkupOptionChange = event => {
        this.setState({
            disableStaticMarkup: event.target.checked
        });
    }

    render() {
        return (
            <>
                {/* Option controls skipped */}
                <div className="ag-theme-balham">
                    <AgGridReact
                        defaultColDef={{
                            width: 90
                        }}
                        components={{
                            vanillaFunction: vanillaFunctionRenderer
                        }}
                        frameworkComponents={{
                            reactClass: ReactClassRenderer,
                            reactFunction: ReactFunctionRenderer
                        }}
                        columnDefs={this.state.columnDefs}
                        rowData={this.state.rowData}
                        disableStaticMarkup={this.state.disableStaticMarkup}
                        onGridReady={this.handleGridReady}
                    />
                </div>
            </>
        );
    }
}

export default Grid;

The last relevant bit of code is the generateColumnsAndRows function used to populate columnDefs and rowData:

const generateColumnsAndRows = (columnsPerTypeCount, rowsCount) => {
    const columnDefs = [];
    const rowData = [];

    for (let i = 0; i < columnsPerTypeCount; i++) {
        columnDefs.push({
            field: 'field_' + i,
            headerName: 'Col ' + i
        }, {
            field: 'field_vf_' + i,
            headerName: 'Col VF ' + i,
            cellRenderer: 'vanillaFunction',
            hide: true
        }, {
            field: 'field_rc_' + i,
            headerName: 'Col RC ' + i,
            cellRenderer: 'reactClass',
            hide: true
        }, {
            field: 'field_rf_' + i,
            headerName: 'Col RF ' + i,
            cellRenderer: 'reactFunction',
            hide: true
        });
    }

    // Row generation skipped

    return [columnDefs, rowData];
}

export default generateColumnsAndRows;

Notice the cellRenderer renderer property and that initially only the columns that don't have any cell renderer assigned are visible.

 

THE RESULTS

Unsurprisingly the fastest way to have a cell value shown is not to have any cell renderer defined. Assuming you need some, the fastest would be a plain JS renderer assigned to components property. If you need to use React component (assigned to frameworkComponents property) then you should stick to a class-based component until ag-Grid team improves function components handling. And BTW, there's nothing wrong with a class, few other things are also easier with class components when it comes to ag-Grid...

Cell rendering performance is one of those things that are best "measured" by eye. Just play with the demo app, switch the columns and scroll the gird and you will notice that in the case of React function component a duplicated value might be briefly visible, spoiling the UX:

Duplicated value... Click to enlarge..


Enabling disableStaticMarkup option helps with duplicated value flicker for function component, but causes an empty value flicker for class based renderer...

If you need something more than your own impression while using the test app, here's a performance measurement done in Chrome 81 DevTools:

Performance measurement in Chrome DevTools... Click to enlarge..

The measurement was done while grid was showing 560 cells on screen with disableStaticMarkup set to false. The timelines show how long it took to fully rerender the grid after renderer type choice was done. Choosing vanilla renderer took about 1300ms, React class used 1900ms and React function took 2600ms. Of course DevTools instrumentation is not free and without it grid would rerender faster - anyways relative speed seems to match the impression my eyes get.