Miłosz Orzeł

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

Suppressing legacy lifecycle method warnings in tests for chosen React components


In preparation for async rendering, React team decided to discourage component authors from relaying on these lifecycle methods: componentWillMountcomponentWillReceiveProps and componentWillUpdate (details).

If you need to use such methods you should prefix their names with UNSAFE_ (for example: UNSAFE_componentWillMount).

Methods without prefix might stop working in React 18.x (some docs are outdated and still say that the drop will happen in version 17)...

If React detects that a component uses legacy method it issues a warning (through call to console.warn) such as this:

Warning: componentWillReceiveProps has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.
    * Move data fetching code or side effects to componentDidUpdate.
    * If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://reactjs.org/link/derived-state
    * Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.
    Please update the following components: LegacyComponent



It's a good thing that React warns about code that uses outdated methods. If you control the code that does so, you should upgrade the names (you can even do it automatically by codemod). But what if you don't? In such case, the warnings and stack traces will clutter output of test runs increasing a chance that more serious issue goes unnoticed.

So if you want/need to suppress the legacy method warnings for selected components, you could add such override to your setupTests.js file (assuming that you've started the app with CRA):

const originalConsoleWarn = console.warn;

// Override to suppress legacy lifecycle method warnings on CHOSEN components. 
console.warn = (...args) => {
    const [firstArg, secondArg] = args;

    // Here's a sample warning we want to skip (line breaks added to limit line length):

    /* ------------------------------------------------------------------------------------------------------- 
    Warning: componentWillMount has been renamed, and is not recommended for use.
    See https://reactjs.org/link/unsafe-component-lifecycles for details.
    * Move code with side effects to componentDidMount, and set initial state in the constructor.
    * Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode.
      In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names,
      you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.
    Please update the following components: AnotherLegacyComponent, LegacyComponent
    ------------------------------------------------------------------------------------------------------- */

    // Notice how the message ends with a list of components which have the legacy method. 
    // This list is provided to console.warn as a second argument which is used to fill %s placeholder
    // in the message passed as the first argument.

    // We want to suppress warnings only on chosen components instead of blindly skipping all unsafe 
    // lifecycle warnings. If warning contains component outside of the skip list, then the message 
    // should not be suppressed!
    const componentsToSkip = ['LegacyComponent', 'AnotherLegacyComponent'];

    const shouldSkip = 
        typeof firstArg === 'string' && // It could also be an object!
        firstArg.includes('In React 18.x, only the UNSAFE_ name will work.') &&
        typeof secondArg === 'string' && // It could also be an object!
        secondArg.split(',').every(name => componentsToSkip.includes(name.trim()));

    !shouldSkip && originalConsoleWarn(...args);

We could simply go for rejecting any warn message that contains a text about _UNSAFE methods but being more picky and skipping warnings only for known components is safer. This way if some new dependency with legacy methods appears in the app, we will know about it. 

I hope the comments in the code above make it clear how the override works. Key thing to note is that console.warn can take multiple arguments (which could be strings or objects) and that React uses second argument to to fill %s placeholder with component names (comma-separated).

Overriding console methods feels a bit hacky, but might be worthwhile if you would like to fail a CI build in case of any warnings... Hopefully one day all components will have upgraded methods names (or switch to function/hooks completely) and this trick will not be needed. A built-in React feature for suppressing such warnings on selected components would be nice too. If there is such thing, please let me know :) 



GitHub repo: https://github.com/morzel85/blog-post-react-test-console-warn-filter (React 17.0.2).

Run npm install followed by npm test to see how the override in setupTest.js suppress warnings for LegacyComponent.js (3 methods) and AnotherLegacyComponent.js (1 method).

WebGPU/WGSL Hello Triangle!


WebGPU is and ongoing effort to make browsers even more powerful platforms. When vendors finish their work (2021/2012?), web apps will be able to leverage the awesome power of modern GPU to render graphics and do general-purpose computing through dedicated API!

High-end games, computer vision, CAD, video processing, machine learning... all done in open, secure and cross-platform way of the Web. Nice. 

Oh, and if you are wondering what's the reason for creating WebGPU while WebGL exists, please check these two articles:



After a bit of digging, I've noticed that some examples posted online no longer work (that's expected), use shading language other than WGSL or may be a bit too complex for a newcomer... So, I've created my own Hello Triangle example (based on austinEng work) that uses just one HTML file and one JS file.

Breaking changes are likely as the specs evolve, so please check the repo for updates.

The code renders properly and runs without warnings in Chrome Canary v91.0.4457.2 x64 Win 10 with --enable-unsafe-webgpu flag set. Tested on 2021-03-25.

Here are the aforementioned files:


<!DOCTYPE html>
<html lang="en">

    <meta charset="utf-8">
    <title>WebGPU/WGSL Hello Triangle!</title>

    <canvas id="canvas" width="400" height="400"></canvas>   
    <script src="script.js"></script>



(async () => {
    if (!navigator.gpu) {
        alert('Your browser does not support WebGPU or it is not enabled. More info: https://webgpu.io');

    const adapter = await navigator.gpu.requestAdapter();
    const device = await adapter.requestDevice();

    const canvas = document.getElementById('canvas')
    const context = canvas.getContext('gpupresent');

    const swapChainFormat = 'bgra8unorm';

    const swapChain = context.configureSwapChain({
        format: swapChainFormat

    const vertexShaderWgslCode =
        const pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
        vec2<f32>(0.0, 0.5),
        vec2<f32>(-0.5, -0.5),
        vec2<f32>(0.5, -0.5));
        [[builtin(position)]] var<out> Position : vec4<f32>;
        [[builtin(vertex_idx)]] var<in> VertexIndex : i32;
        fn main() -> void {
            Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);

    const fragmentShaderWgslCode =
        [[location(0)]] var<out> outColor : vec4<f32>;
        fn main() -> void {
            outColor = vec4<f32>(0.0, 1.0, 0.0, 1.0);

    const pipeline = device.createRenderPipeline({
        vertex: {
            module: device.createShaderModule({
                code: vertexShaderWgslCode
            entryPoint: 'main'
        fragment: {
            module: device.createShaderModule({
                code: fragmentShaderWgslCode
            entryPoint: 'main',
            targets: [{
                format: swapChainFormat,
        primitive: {
            topology: 'triangle-list',

    const commandEncoder = device.createCommandEncoder();
    const textureView = swapChain.getCurrentTexture().createView();

    const renderPassDescriptor = {
        colorAttachments: [{
            attachment: textureView,
            loadValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },

    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
    passEncoder.draw(3, 1, 0, 0);


The code uses WebGPU with WebGPU Shading Language to render a green triangle on a gray background inside a 400x400 px canvas. 

All this stuff to get a triangle? Don't worry :) Remember that WebGPU is a low-level API. Your favorite libraries like tree.js or babylon.js will use it internally.



There has been a bit of controversy surrounding the choice of creating WGSL instead of going for widely supported GLSL... You might want to check this issue and this comment in particular (from one of the main contributors to WebGPU) to see the rationale.


Safer ag-Grid (React) Column Definitions with TypeScript


In JavaScript, an ag-Grid column is bound to a row property by assigning a string to field config. This is quite fragile: a typo or change in row model will cause and empty column in the grid. Fortunately, a much safer binding is possible thanks to TypeScript's keyof operator. Check grid component example and demo page.



I'm continuing my little (lockdown-boredom driven) series on ag-Grid usage with React. Previous posts (hooks, resize, renderers) were done with JavaScript, now it's time to present benefits TypeScript!

ag-Grid docs show an easy way of defining the binding between column and a row object property:

columnDefs: [
    { headerName: 'Athlete', field: 'athlete' } 

With the above column definition, the grid will look for athlete property (in objects passed as row data) to populate the Athlete column. Such column biding is very easy to setup but it's also easy to break. If you make a typo or change property name in row data, the grid will render empty cells! You might try to remedy this with constans but why not let the TypeScript do the work for you?

Assuming that your React project has TypeScript enabled (easy-peasy with CRA), making type-safe column bindings requires just two things:

  • declaring a type (or an interface) that models grid rows,
  • using property name retrieved with keyof to set column's field.



Let's say we want to show a grid with a list of brightest starts (live version):

Brightest stars grid... Click to enlarge...

The grid has 6 columns, so let's create Star.ts file with a type that models the rows:

type Star = {
    rank: number;
    magnitude: number;
    name: string;
    designation: string;
    distance: number;
    spectral: string;

export default Star;

Once we have the type, we can use it to define grid columns. Below is the entire (tsx) code needed to define grid component (in functional flavor):

import React from 'react';
import { ColDef } from 'ag-grid-community';
import { AgGridReact } from 'ag-grid-react';

import Star from './Star';
import brightestStars from './brightestStars'

import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-balham.css';

const fieldName = (name: keyof Star) => name;

const columnDefs: ColDef[] = [
        headerName: 'Rank',
        field: fieldName('rank'),
        width: 80
        headerName: 'Visual magnitude (mV)',
        field: fieldName('magnitude'),
        width: 170
        headerName: 'Proper name',
        field: fieldName('name'),
        width: 180
        headerName: 'Bayer designation',
        field: fieldName('designation'),
        width: 150
        headerName: 'Distance (ly)',
        field: fieldName('distance'),
        width: 120
        headerName: 'Spectral class',
        field: fieldName('spectral'),
        width: 130

const StarsGrid: React.FC = () => {
    return (
        <div className='ag-theme-balham'>
                    sortable: true

export default StarsGrid;

Have you noticed that the file has an import of ColDef from ag-grid-community package? This is not required but ag-Grid comes with type definitions and these make development a lot easier. Because columnDefs is declared as an array of ColDef the IDE (I'm using Visual Studio Code) is able to offer suggestions on column properties and will instantly highlight any mistake:

ColDef type sugestions...

Notice that the Star type is imported too. It is needed in the line that provides static names of Star properties:

const fieldName = (name: keyof Star) => name;

This little arrow function is later used while field mapping is defined:

    headerName: 'Visual magnitude (mV)',
    field: fieldName('magnitude'),
    width: 170,

At first glance it doesn't look very useful... The 'magnitude' is still a string? Let's see what happens if 'dsignation' typo is made and Star model is modified by removing the spectral property:

Mapping errors detected... Click to enlarge...

IDE immediately highlights the errors and TypeScript won't compile. Static typing FTW!



The app was built in React 16.13.1, ag-Grid Community 23.1.1TypeScript 3.7.0 and tested in Chrome 83, Firefox 76 and Edge 44.

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)...



TypeScript 2.1 introduced keyof operator that creates a type that lists property names. In our case keyof Star yields a type that has a union of strings containing 6 property names that exist in Star. In fact you could create such type manually:

type TheNames = "rank" | "magnitude" | "name" | "designation" | "distance" | "spectral";

and it would be the same as a type created with: 

type TheNames = keyof Star;

The keyof option is clearly a better choice because the type will be amended automatically whenever Star type changes its properties.

The fieldName arrow function used in column mappings makes use of keyof to limit the acceptable strings:

const fieldName = (name: keyof Star) => name;

Thanks to keyof, TypeScript will reject any string passed to fieldName function which does not belong to a union of Star property names. Nice, no runtime supersizes!

ag-Grid (React) Cell Renderers Performance


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.



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) {

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

        this.state = {
            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 => {
            disableStaticMarkup: event.target.checked

    render() {
        return (
                {/* Option controls skipped */}
                <div className="ag-theme-balham">
                            width: 90
                            vanillaFunction: vanillaFunctionRenderer
                            reactClass: ReactClassRenderer,
                            reactFunction: ReactFunctionRenderer

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++) {
            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.



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.

Resizing All ag-Grid (React) Columns


You can use measureText method to calculate widths of text and then use setColumnWidth to adjust columns. This the demo and this is the repo. It's quite likely that you don't need this technique (suppressColumnVirtualisation might be enough), but read on if your grid has huge amount of columns and users need high data density...



I've been porting some old UI with grids based on HTML table to ag-Grid. Users appreciate the features added by ag-Grid (such as filters, pinning, sorting, resizing, reordering, grouping...) but noticed one change for worse: not all columns were automatically sized to take the smallest amount of space possible. In enterprise applications data density is really important (not recognizing that is a recipe for failure). Too much white space means that people might miss important information or have to waste time scrolling...

ag-Grid is is capable of handing huge amount of rows and columns thanks to virtualization. The gird creates elements only for rows and columns which are currently visible (plus some buffer) to avoid producing bloated DOM (too many elements severely degrade performance). Virtualization means that the API methods designed to automatically resize columns are only capable of resizing the rendered columns. ag-Grid has suppressColumnVirtualisation option but in my case girds can have as much as 300 columns so turning virtualization off is not possible for performance reasons.



So is it possible to resize all columns while enjoying the benefits of virtualization? If your grid cells contain only text and you know the font then yes, it's possible.

You can calculate the width of text for each grid cell and collect the minimal width needed for each of the gird columns. ag-Grid API might then be used to set desired column sizes. Text width calculation could be done efficiently with measureText method on Canvas.

Here's a module that can collect minimal width needed for each column (it doesn't have any dependency on React):

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

const collectMaxWidth = (text, group, maxWidths) => {
    const width = context.measureText(text).width;

    const maxWidth = maxWidths.get(group);

    if (maxWidth === undefined || width > maxWidth) {
        maxWidths.set(group, width);

const collectMaxWidthCached = (text, group, maxWidths, widthsCache) => {
    const cachedWidth = widthsCache.get(text);

    let width;

    if (cachedWidth === undefined) {
        width = context.measureText(text).width;
        widthsCache.set(text, width);
    } else {
        width = cachedWidth;

    const maxWidth = maxWidths.get(group);

    if (maxWidth === undefined || width > maxWidth) {
        maxWidths.set(group, width);

const calculateColumnWidths = config => {
    console.time('Column widths calculation');

    const maxWidths = new Map()

    if (config.measureHeaders) {
        context.font = config.headerFont;

        config.columnDefs.forEach(column => {
            collectMaxWidth(column.headerName, column.field, maxWidths);

    context.font = config.rowFont;

    config.rowData.forEach(row => {
        config.columnDefs.forEach(column => {
            if (config.cache) {
                collectMaxWidthCached(row[column.field], column.field, maxWidths, config.cache);
            } else {
                collectMaxWidth(row[column.field], column.field, maxWidths);

    const updatedColumnDefs = config.columnDefs.map(cd => ({
        width: Math.ceil(maxWidths.get(cd.field) + config.padding)

    console.timeEnd('Column widths calculation');

    return updatedColumnDefs;

export default calculateColumnWidths;

The module creates canvas element and then a 2d context is retrieved from it. The context is used in collectMaxWidth function to measure size of provided text by invoking context.measureText(text).width

The module also has collectMaxWidthCached function which can offer a performance improvement based on the fact that grid data is often repetitive. If some string was already measured, there's no need to use canvas API again - taking the value from JavaScript Map is super quick. So unless you are extremely worried about memory limits, use the cached version.

The module exports calculateColumnWidths function which takes config object with following properties:

  • columnDefs - Array used to specify ag-Grid columns (limit this if you have hidden columns).
  • rowData - Array of grid records (limit this if you use paging or filtering).
  • measureHeaders - If true then column headers should be taken into account (caveat: header icons are ignored). 
  • headerFont - Determines column header font.
  • rowFont - Determines normal grid cell font.
  • padding - Additional width added to measured text (you need to choose it experimentally, mind varying header icons).
  • cache - JS Map used for speed boost (length of each unique non-header text is measured only once), pass null to skip caching.

Below is an example of button's click handler from my demo app what uses calculateColumnWidths:

const handleResizeWithCustomClick = () => {
    console.time('Resize all columns (including widths calculation)');

    if (gridApi && gridColumnApi) {
        // Here ALL columns and rows are used because there are no hidden columns
        // and the the grid has neither paging nor filtering enabled!
        const updatedColumDefs = calculateColumnWidths({
            measureHeaders: true,
            headerFont: 'bold 12px Arial',
            rowFont: 'normal 12px Arial',
            padding: 30,
            cache: useWidthsCache ? textWidthsCache : null

        // Setting width by setColumnWidth has the advantage of preserving column
        // changes done by user such as sorting or filters. The disadvantage is that
        // initial resize might be slow if the grid was scrolled towards later columns
        // before resizing was invoked (bug in the gird?).
        // Resize by gridApi.setColumnDefs(updatedColumDefs) or setColumnDefs(updatedColumDefs)
        // should be faster but columns settings could be reset (mind deltaColumnMode)...            
        updatedColumDefs.forEach(def => gridColumnApi.setColumnWidth(def.field, def.width));

    console.timeEnd('Resize all columns (including widths calculation)');

Notice how setColumnWidth from ag-Grid Column API is used to apply calculated width. Mind the comments about paging/filtering and the difference between using setColumnWidth and calling Grid API setColumnDefs or updating state bound to grid's columnDefs property...



Live demo: https://morzel85.github.io/blog-post-ag-grid-full-resize 
Source code on GitHub: https://github.com/morzel85/blog-post-ag-grid-full-resize

The app uses React 16.13.1 and ag-Grid Community 23.0.1 (I've tested it in Chrome 80, Firefox 74, Edge 44).
Clone the repo, do npm install and npm start to run the app locally (just like with any other thing started with create-react-app)...

Click on "Generate data (100 rows with 300 columns)" button to create 30K grid cells. You should then click the "Resize columns with columnApi.autoSizeColumns" button and scroll to the right to notice that only visible columns were resized:

Resizing by autoSizeColumns... Click to enlarge...

Now reload the page, click data generation again and press "Resize columns with custom text measure" and scroll the gird horizontally. You should notice that all columns were resized:

Resizing by measureText... Click to enlarge...

Clicking resizing button the the second time should give you better performance because of text measure caching and because ag-Grid itself has less work to do. These are the timings from Chrome: 

Resizing perfromance in Chrome... Click to enlarge...
I would say that 100ms for 30K cell measure is quite fast! With caching it drops to 5ms! You may also notice that most of the time is not really spent in my text measuring function but in the ag-Grid which handles columns resize. 

I've noticed one weird performance issue: if you scroll to the right of the grid before clicking the resize button, the time spent by ag-Grid handling columnApi.autoSizeColumns calls increases significantly. Doing bulk resize with gridApi.setColumnDefs or by updating array used in for columnDefs property solves the performance issue at the cost of column settings reset (exact behavior depends on deltaColumnMode)...

Update 2020-13-04:
ag-Grid team confirmed that there's indeed an issue with setColumnWidth performance and the fix should be ready later this month.