Performance Tuning Large Grids and Stores in Ext JS
Get a summary of this article:
Grids are where enterprise Ext JS applications live, and they are also where performance problems show up first. A screen that felt instant with 200 rows becomes sluggish with 20,000, and users blame the framework. In practice, the slowdown almost always comes from a handful of avoidable patterns: loading too much data, doing too much work per row, and triggering more layouts and events than necessary.
This article walks through the most effective fixes, in the order you should apply them. The snippets target Ext JS 7 (classic toolkit).

1. Measure before you tune
It is tempting to start by sprinkling optimizations. Resist that. Open the browser’s performance profiler, record the slow interaction, and find out where the time goes. In grid-heavy screens it is usually one of four things:
- Too much data transferred and parsed.
- Too many DOM nodes rendered.
- Expensive column renderers running on every row.
- Repeated layouts and store events during bulk updates.
Each section below targets one of these. Knowing which one you have saves you from optimizing the wrong thing.
2. Do not load what the user cannot see
The fastest data is data you never request. Decide early between two strategies.
Classic paging works well when users think in pages and total counts are cheap:
Ext.define('Acme.store.Accounts', {
extend: 'Ext.data.Store',
alias: 'store.accounts',
model: 'Acme.model.Account',
pageSize: 50,
remoteSort: true,
remoteFilter: true,
autoLoad: true
});
Buffered scrolling works better for long, continuous lists such as ledger entries, where users scroll rather than page. A buffered store requests slices of data as the user scrolls and the grid renders only the visible rows:
Ext.define('Acme.store.LedgerEntries', {
extend: 'Ext.data.BufferedStore',
alias: 'store.ledgerentries',
model: 'Acme.model.LedgerEntry',
pageSize: 100,
leadingBufferZone: 300,
trailingBufferZone: 100,
remoteSort: true,
remoteFilter: true,
autoLoad: true
});
Both approaches depend on the server doing the heavy lifting. With remoteSort and remoteFilter enabled, sorting and filtering happen in the database, where indexes help, instead of in the browser across tens of thousands of records. Your API needs to accept the paging parameters and return the total count, so make sure the reader’s totalProperty is configured.
3. Keep the DOM small
Rendering is expensive. The grid’s buffered renderer keeps only the rows near the viewport in the DOM, which is what makes large datasets practical. It is enabled by default for the classic grid, so the main job is to avoid breaking it:
- Prefer fixed row heights. Variable row heights force the grid to measure rows and recalculate scroll positions. Keep variableRowHeight off (the default) unless you really need wrapped content.
- Avoid very wide grids. Dozens of columns multiply the work per row. Hide rarely used columns by default and let users opt in.
- Avoid nesting heavy components inside cells. Widget columns and row-expander content are powerful, but each one is a full component. Use them where they add real value.
Ext.define('Acme.view.ledger.LedgerGrid', {
extend: 'Ext.grid.Panel',
xtype: 'ledgergrid',
bind: { store: '{entries}' },
// Buffered rendering is on by default; shown here for clarity
bufferedRenderer: true,
columns: [
{ text: 'Date', dataIndex: 'date', xtype: 'datecolumn', width: 110 },
{ text: 'Account', dataIndex: 'accountCode', width: 120 },
{ text: 'Description', dataIndex: 'description', flex: 1 },
{ text: 'Amount', dataIndex: 'amount', width: 130, align: 'right',
renderer: 'amountRenderer' },
// Rarely used, so hidden by default
{ text: 'Cost Center', dataIndex: 'costCenter', hidden: true }
]
});
4. Keep renderers cheap
A column renderer runs for every visible cell, every time the row refreshes. Scrolling, sorting, and updates all trigger it. A slow renderer therefore costs you (rows × columns × refreshes) times its own cost.
The common mistakes are creating objects or formatters on each call, doing lookups by scanning arrays, and building complex HTML strings. Move the setup out of the renderer and precompute what you can.
Slow:
renderer: function (value, meta, record) {
// Creates a new formatter and scans a store on every cell
var account = Ext.getStore('accounts').findRecord('code', record.get('accountCode'));
return Ext.util.Format.number(value, '0,000.00') + ' (' + account.get('description') + ')';
}
Better:
// Look-ups built once, formatting kept trivial
renderer: function (value) {
return Ext.util.Format.number(value, '0,000.00');
}
If a renderer needs related data, denormalize it into the record on the server or use a model convert/calculate field computed once on load, not once per paint. When you must look something up, build a keyed map a single time:
Ext.define('Acme.view.ledger.LedgerGridController', {
extend: 'Ext.app.ViewController',
alias: 'controller.ledgergrid',
amountRenderer: function (value) {
return Ext.util.Format.number(value, '0,000.00');
}
});
Using a named renderer on the controller (as in the grid above) also keeps the view declarative and makes the function easy to unit test.
5. Batch your updates
When code changes many records or components at once, each change can trigger store events, view refreshes, and layouts. Batching collapses that into one pass.
Add records in bulk, not in a loop:
// Triggers an event and refresh per record
rows.forEach(function (data) {
store.add(data);
});
// One add, one refresh
store.add(rows);
Bracket multi-step changes so listeners fire once at the end:
store.beginUpdate();
try {
records.forEach(function (rec) {
rec.set('status', 'closed');
});
} finally {
store.endUpdate();
}
Suspend layouts when you are modifying the component tree, such as adding several items to a container:
Ext.suspendLayouts();
try {
container.removeAll();
container.add(newItems);
} finally {
Ext.resumeLayouts(true);
}
The try/finally matters: if something throws between suspend and resume, you do not want the UI stuck with layouts permanently suspended.
6. Throttle the user’s own input
A filter field that reloads or refilters on every keystroke can generate dozens of requests or expensive local filters per second. Add a buffer to the listener so it fires once the user pauses:
{
xtype: 'textfield',
emptyText: 'Filter by description',
listeners: {
change: {
fn: 'onFilterChange',
buffer: 300
}
}
}
onFilterChange: function (field, value) {
var store = this.getViewModel().getStore('entries');
if (value) {
store.filter('description', value);
} else {
store.clearFilter();
}
}
With a remote filter, the same buffer also protects your server from a burst of redundant queries.
7. Fetch only the fields you need
Every field in a model has a cost: network payload, JSON parsing, record creation, and memory. If a list screen needs six properties and your endpoint returns forty, trim the response on the server or expose a lighter list endpoint and reserve the full record for the detail view. A grid does not need the entire entity.
The same goes for model fields. Fields marked persist: false or calculate fields that depend on many others are recomputed when their dependencies change, so keep calculated fields few and cheap.
8. Clean up after yourself
Performance degrades over a long session if screens leak. A user who opens and closes forty tabs should not leave forty grids’ worth of stores, listeners, and DOM behind.
- Destroy components you remove: container.remove(item, true) destroys by default for most containers, but check when you hold references yourself.
- Destroy stores you create in code with Ext.destroy(store). Stores owned by a view model are cleaned up with the view model.
- Remove listeners you attach to long-lived objects, such as global stores or Ext.GlobalEvents, using destroyable listeners or mon() on the component so they are removed with it.
A short checklist
- Did you profile first, and do you know which of the four causes you are fixing?
- Are paging or buffered scrolling in place, with sorting and filtering done on the server?
- Are rows a fixed height, with rarely used columns hidden by default?
- Are renderers free of lookups, object creation, and heavy string building?
- Are bulk changes wrapped in beginUpdate/endUpdate or suspendLayouts?
- Do filter inputs use a buffer, and do list endpoints return only the fields the list needs?
- Does closing a screen destroy its components, stores, and listeners?
Most large-grid problems are solved by the first two items. The rest are what keep a fast screen fast after a year of feature work.
Many of today’s most important business applications started life in the era of the JavaScript…
“What’s the best JavaScript framework?” has no single answer, because frameworks are built for different…
Choosing a React JS framework is the first decision in enterprise application development, not the…




