Grid allows you to load large amount of data without performance degradation.
To use virtualization, you need to inject VirtualScroll
module in grid.
Row virtualization allows you to load and render rows only in the content viewport. It is an alternative way of paging in which the data will be loaded while scrolling vertically. To setup the row virtualization, you need to define
enableVirtualization
as true and content height by height
property.
The number of records displayed in the Grid is determined implicitly by height of the content area. Also, you have an option to define a visible number of records by
the pageSettings.pageSize
property. The loaded data will be cached and reused when it is needed for next time.
var names = ['TOM', 'Hawk', 'Jon', 'Chandler', 'Monica', 'Rachel', 'Phoebe', 'Gunther', 'Ross', 'Geller', 'Joey', 'Bing', 'Tribbiani', 'Janice', 'Bong', 'Perk', 'Green', 'Ken', 'Adams'];
var hours = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var designation = ['Manager', 'Engineer 1', 'Engineer 2', 'Developer', 'Tester'];
var statusValue = ['Completed', 'Open', 'In Progress', 'Review', 'Testing']
var data = function(count) {
var result = [];
for (var i = 0; i < count; i++) {
result.push({
TaskID: i + 1,
Engineer: names[Math.round(Math.random() * names.length)] || names[0],
Designation: designation[Math.round(Math.random() * designation.length)] || designation[0],
Estimation: hours[Math.round(Math.random() * hours.length)] || hours[0],
Status: statusValue[Math.round(Math.random() * statusValue.length)] || statusValue[0]
});
}
return result;
};
(window).getStatus = function(statusValue){
var colors = { 'Completed': 'green', 'Open': 'red', 'In Progress': '#FB1E77', 'Review': 'brown', 'Testing': '#1EC1FB' };
return '<span style="color:' + colors[statusValue] + '">' + statusValue + '</span>';
};
ej.grids.Grid.Inject(ej.grids.VirtualScroll, ej.grids.Edit, ej.grids.Toolbar);
var grid = new ej.grids.Grid({
dataSource: data(1000),
height: 300,
enableVirtualization: true,
pageSettings: { pageSize: 50 },
editSettings: { allowAdding: true, allowEditing: true, allowDeleting: true, mode: 'Normal' },
toolbar: ['Add', 'Edit', 'Delete', 'Update', 'Cancel'],
columns: [
{ field: 'TaskID', headerText: 'Task ID', textAlign: 'Right', width: 100, type: 'number', isPrimaryKey: true, validationRules: { required: true } },
{ field: 'Engineer', width: 100 },
{ field: 'Designation', width: 140, editType: 'dropdownedit', validationRules: { required: true } },
{ field: 'Estimation', textAlign: 'Right', width: 110, editType: 'numericedit', validationRules: { required: true } },
{ field: 'Status', width: 140, template: '${getStatus(data.Status)}', editType: 'dropdownedit' }
]
});
grid.appendTo('#Grid');
<!DOCTYPE html><html lang="en"><head>
<title>EJ2 Grid</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Typescript Grid Control">
<meta name="author" content="Syncfusion">
<link href="index.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-base/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-grids/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-buttons/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-popups/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-richtexteditor/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-navigations/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-dropdowns/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-lists/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-inputs/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-calendars/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-notifications/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-splitbuttons/styles/material.css" rel="stylesheet">
<style>
.e-row[aria-selected="true"] .e-customizedExpandcell {
background-color: #e0e0e0;
}
.e-grid.e-gridhover tr[role='row']:hover {
background-color: #eee;
}
.e-expand::before {
content: '\e5b8';
}
.empImage {
margin: 6px 16px;
float: left;
width: 50px;
height: 50px;
}
</style>
<script src="https://cdn.syncfusion.com/ej2/21.2.3/dist/ej2.min.js" type="text/javascript"></script>
<script src="es5-datasource.js" type="text/javascript"></script>
</head>
<body>
<div id="container">
<div id="Grid"></div>
</div>
<script>
var ele = document.getElementById('container');
if(ele) {
ele.style.visibility = "visible";
}
</script>
<script src="index.js" type="text/javascript"></script>
</body></html>
Column virtualization allows you to virtualize columns. It will render columns which are in the viewport. You can scroll horizontally to view more columns.
To setup the column virtualization, set the
enableVirtualization
and
enableColumnVirtualization
properties as true
.
var virtualData = [];
var names = ['hardire', 'abramjo01', 'aubucch01', 'Hook', 'Rumpelstiltskin', 'Belle', 'Emma', 'Regina', 'Aurora', 'Elsa', 'Anna', 'Snow White', 'Prince Charming', 'Cora', 'Zelena', 'August', 'Mulan', 'Graham', 'Discord', 'Will', 'Robin Hood', 'Jiminy Cricket', 'Henry', 'Neal', 'Red', 'Aaran', 'Aaren', 'Aarez', 'Aarman', 'Aaron', 'Aaron-James', 'Aarron', 'Aaryan', 'Aaryn', 'Aayan', 'Aazaan', 'Abaan', 'Abbas', 'Abdallah', 'Abdalroof', 'Abdihakim', 'Abdirahman', 'Abdisalam', 'Abdul', 'Abdul-Aziz', 'Abdulbasir', 'Abdulkadir', 'Abdulkarem', 'Abdulkhader', 'Abdullah', 'Abdul-Majeed', 'Abdulmalik', 'Abdul-Rehman', 'Abdur', 'Abdurraheem', 'Abdur-Rahman', 'Abdur-Rehmaan', 'Abel', 'Abhinav', 'Abhisumant', 'Abid', 'Abir', 'Abraham', 'Abu', 'Abubakar', 'Ace', 'Adain', 'Adam', 'Adam-James', 'Addison', 'Addisson', 'Adegbola', 'Adegbolahan', 'Aden', 'Adenn', 'Adie', 'Adil', 'Aditya', 'Adnan', 'Adrian', 'Adrien', 'Aedan', 'Aedin', 'Aedyn', 'Aeron', 'Afonso', 'Ahmad', 'Ahmed', 'Ahmed-Aziz', 'Ahoua', 'Ahtasham', 'Aiadan', 'Aidan', 'Aiden', 'Aiden-Jack', 'Aiden-Vee'];
function dataSource(){
for (var i = 0; i < 1000; i++) {
virtualData.push({
'SNo': i + 1,
'FIELD1': names[Math.floor(Math.random() * names.length)],
'FIELD2': 1967 + (i % 10), 'FIELD3': Math.floor(Math.random() * 200),
'FIELD4': Math.floor(Math.random() * 100), 'FIELD5': Math.floor(Math.random() * 2000), 'FIELD6': Math.floor(Math.random() * 1000), 'FIELD7': Math.floor(Math.random() * 100), 'FIELD8': Math.floor(Math.random() * 10), 'FIELD9': Math.floor(Math.random() * 10), 'FIELD10': Math.floor(Math.random() * 100), 'FIELD11': Math.floor(Math.random() * 100), 'FIELD12': Math.floor(Math.random() * 1000), 'FIELD13': Math.floor(Math.random() * 10), 'FIELD14': Math.floor(Math.random() * 10), 'FIELD15': Math.floor(Math.random() * 1000), 'FIELD16': Math.floor(Math.random() * 200), 'FIELD17': Math.floor(Math.random() * 300), 'FIELD18': Math.floor(Math.random() * 400), 'FIELD19': Math.floor(Math.random() * 500), 'FIELD20': Math.floor(Math.random() * 700), 'FIELD21': Math.floor(Math.random() * 800), 'FIELD22': Math.floor(Math.random() * 1000), 'FIELD23': Math.floor(Math.random() * 2000), 'FIELD24': Math.floor(Math.random() * 150), 'FIELD25': Math.floor(Math.random() * 1000), 'FIELD26': Math.floor(Math.random() * 100), 'FIELD27': Math.floor(Math.random() * 400), 'FIELD28': Math.floor(Math.random() * 600), 'FIELD29': Math.floor(Math.random() * 500), 'FIELD30': Math.floor(Math.random() * 300),
});
}
}
dataSource();
ej.grids.Grid.Inject(ej.grids.VirtualScroll, ej.grids.Edit, ej.grids.Toolbar);
var grid = new ej.grids.Grid({
dataSource: virtualData,
enableVirtualization: true,
enableColumnVirtualization: true,
height: 300,
editSettings: { allowAdding: true, allowEditing: true, allowDeleting: true, mode: 'Normal' },
toolbar: ['Add', 'Edit', 'Delete', 'Update', 'Cancel'],
columns: [
{ field: 'SNo', headerText: 'S.No', width: 120, isPrimaryKey: true, validationRules: { required: true } },
{ field: 'FIELD1', headerText: 'Player Name', width: 140, editType: 'dropdownedit', validationRules: { required: true } },
{ field: 'FIELD2', headerText: 'Year', width: 120, textAlign: 'Right' },
{ field: 'FIELD3', headerText: 'Stint', width: 120, textAlign: 'Right' },
{ field: 'FIELD4', headerText: 'TMID', width: 120, textAlign: 'Right' },
{ field: 'FIELD5', headerText: 'LGID', width: 120, textAlign: 'Right' },
{ field: 'FIELD6', headerText: 'GP', width: 120, textAlign: 'Right' },
{ field: 'FIELD7', headerText: 'GS', width: 120, textAlign: 'Right' },
{ field: 'FIELD8', headerText: 'Minutes', width: 120, textAlign: 'Right' },
{ field: 'FIELD9', headerText: 'Points', width: 120, textAlign: 'Right' },
{ field: 'FIELD10', headerText: 'oRebounds', width: 130, textAlign: 'Right' },
{ field: 'FIELD11', headerText: 'dRebounds', width: 130, textAlign: 'Right' },
{ field: 'FIELD12', headerText: 'Rebounds', width: 120, textAlign: 'Right' },
{ field: 'FIELD13', headerText: 'Assists', width: 120, textAlign: 'Right' },
{ field: 'FIELD14', headerText: 'Steals', width: 120, textAlign: 'Right' },
{ field: 'FIELD15', headerText: 'Blocks', width: 120, textAlign: 'Right' },
{ field: 'FIELD16', headerText: 'Turnovers', width: 130, textAlign: 'Right' },
{ field: 'FIELD17', headerText: 'PF', width: 130, textAlign: 'Right' },
{ field: 'FIELD18', headerText: 'fgAttempted', width: 150, textAlign: 'Right' },
{ field: 'FIELD19', headerText: 'fgMade', width: 120, textAlign: 'Right' },
{ field: 'FIELD20', headerText: 'ftAttempted', width: 150, textAlign: 'Right' },
{ field: 'FIELD21', headerText: 'ftMade', width: 120, textAlign: 'Right' },
{ field: 'FIELD22', headerText: 'ThreeAttempted', width: 150, textAlign: 'Right' },
{ field: 'FIELD23', headerText: 'ThreeMade', width: 130, textAlign: 'Right' },
{ field: 'FIELD24', headerText: 'PostGP', width: 120, textAlign: 'Right' },
{ field: 'FIELD25', headerText: 'PostGS', width: 120, textAlign: 'Right' },
{ field: 'FIELD26', headerText: 'PostMinutes', width: 120, textAlign: 'Right' },
{ field: 'FIELD27', headerText: 'PostPoints', width: 130, textAlign: 'Right' },
{ field: 'FIELD28', headerText: 'PostoRebounds', width: 130, textAlign: 'Right' },
{ field: 'FIELD29', headerText: 'PostdRebounds', width: 130, textAlign: 'Right' },
{ field: 'FIELD30', headerText: 'PostRebounds', width: 130, textAlign: 'Right', editType: 'numericedit', validationRules: { required: true } }]
});
grid.appendTo('#Grid');
<!DOCTYPE html><html lang="en"><head>
<title>EJ2 Grid</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Typescript Grid Control">
<meta name="author" content="Syncfusion">
<link href="index.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-base/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-grids/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-buttons/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-popups/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-richtexteditor/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-navigations/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-dropdowns/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-lists/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-inputs/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-calendars/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-notifications/styles/material.css" rel="stylesheet">
<link href="//cdn.syncfusion.com/ej2/21.2.3/ej2-splitbuttons/styles/material.css" rel="stylesheet">
<style>
.e-row[aria-selected="true"] .e-customizedExpandcell {
background-color: #e0e0e0;
}
.e-grid.e-gridhover tr[role='row']:hover {
background-color: #eee;
}
.e-expand::before {
content: '\e5b8';
}
.empImage {
margin: 6px 16px;
float: left;
width: 50px;
height: 50px;
}
</style>
<script src="https://cdn.syncfusion.com/ej2/21.2.3/dist/ej2.min.js" type="text/javascript"></script>
<script src="es5-datasource.js" type="text/javascript"></script>
</head>
<body>
<div id="container">
<div id="Grid"></div>
</div>
<script>
var ele = document.getElementById('container');
if(ele) {
ele.style.visibility = "visible";
}
</script>
<script src="index.js" type="text/javascript"></script>
</body></html>
Column’s
width
is required for column virtualization. If column’s width is not defined then Grid will consider its value as200px
.
Both the row and column virtualization can be used along with grouping. At initial rendering, the virtual height of scrollbar will be set based on the total number of records and after grouping, it will be refreshed based on the grouped state(expand/collapse). While collapse the group caption row in current viewport then the next view page grouped records are shown.
The collapsed/expanded state will persist only for local dataSource while scrolling.
Group with Page
topic..e-grid .e-row {
height: 2em;
}
selectRows
method is not supported in virtual scrolling.You can load millions of records in the Grid by using virtual scrolling, where the grid loads and renders rows on-demand while scrolling vertically. As a result, Grid lightens the browser’s load by minimizing the DOM elements and rendering elements visible in the viewport. The height of the grid is calculated using the Total Records Count * Row Height property.
The browser has some maximum pixel height limitations for the scroll bar element. The content placed above the maximum height can’t be scrolled if the element height is greater than the browser’s maximum height limit. The browser height limit affects the virtual scrolling of the grid. When a large number of records are bound to the Grid, it can only display the records until the maximum height limit of the browser. Once the browser’s height limit is reached while scrolling, the user won’t able to scroll further to view the remaining records.
For example, if the row height is set as 30px and the total record count is 1000000(1 million), then the height of the grid element will be 30,000,000 pixels. In this case, the browser’s maximum height limit for a div is about 22,369,600 (The maximum pixel height limitation differs for different browsers). The records above the maximum height limit of the browser can’t be scrolled.
This height limitation is not related to the Grid component. It fully depends on the default behavior of the browser. The same issue is reproduced in the normal HTML table too.
The following image illustrates the height limitation issue of a normal HTML table in different browsers (Chrome and Firefox).
Grid component also faced the same issue as mentioned in the below image.
The Grid has an option to overcome this limitation of the browser in the following ways.
You can prevent the height limitation problem in the browser when scrolling through millions of records by loading the segment of data through different strategy.
In the following sample, Grid is rendered with a large number of records(nearly 2 million). Here, you can scroll 0.5 million records at a time in Grid. Once you reach the last page of 0.5 million records, the Load Next Set button will be shown at the bottom of the Grid. By clicking that button, you can view the next set of 0.5 million records in Grid. Also, the Load Previous Set button will be shown at the top of the Grid to load the previous set of 0.5 million records.
Let’s see the step by step procedure for how we can overcome the limitation in the Syncfusion Grid component.
class CustomUrlAdaptor extends UrlAdaptor {
processQuery(args) {
if (arguments[1].queries) {
for (var i = 0; i < arguments[1].queries.length; i++) {
if (arguments[1].queries[i].fn === 'onPage') {
// pageSet - defines the number of segments that we are going to split the 2million records. In this example we have considered 0.5 million records for each set so the pageSet is 1, 2, 3 and 4.
// maxRecordsPerPageSet – In this example we define the value as 0.5 million.
// gridPageSize – the pageSize that we have defined in the Grid pageSettings->pageSize property
// customize the pageIndex based on the current pageSet (It send the skip query including the previous pageSet ) so that the other operations performed for total 2millions records instead of 0.5 million alone.
arguments[1].queries[i].e.pageIndex = (((pageSet - 1) * maxRecordsPerPageSet) / gridPageSize) + arguments[1].queries[i].e.pageIndex;
}
}
}
let original = super.processQuery.apply(this, arguments);
return original;
}
}
let data: DataManager = new DataManager({
adaptor: new CustomUrlAdaptor,
url: "Home/UrlDatasource"
});
let grid: Grid = new Grid({
dataSource: data,
enableVirtualization: true,
pageSettings: {pageSize: 50},
height: 360,
beforeDataBound: beforeDataBound,
columns: [
{ field: 'OrderID', width: 120, headerText: 'Order ID', textAlign: 'Right' }
......
......
]
});
beforeDataBound(args) {
// storing the total records count which means 2 million records count
totalRecords = args.count;
// change the count with respect to maxRecordsPerPageSet (maxRecordsPerPageSet = 500000)
args.count = args.count - ((pageSet - 1) * maxRecordsPerPageSet) > maxRecordsPerPageSet ?maxRecordsPerPageSet : args.count - ((pageSet - 1) * maxRecordsPerPageSet);
}
let button: Button = new Button({
cssClass: 'e-info prevbtn',
onClick: prevBtnClick,
content: 'Load Previous Set...'
});
let grid: Grid = new Grid({
dataSource: data,
enableVirtualization: true,
pageSettings: {pageSize: 50},
height: 360,
beforeDataBound: beforeDataBound,
columns: [
{ field: 'OrderID', width: 120, headerText: 'Order ID', textAlign: 'Right' }
......
......
]
});
let button: Button = new Button({
cssClass: 'e-info nxtbtn',
onClick: nxtBtnClick,
content: 'Load Next Set...'
});
Load Next Set
/ Load Previous Set
button corresponding page data set is loaded to view remaining records of total 2 millions records after doing some simple calculation. // Triggered when clicking the Previous/ Next button.
prevNxtBtnClick(args) {
if (grid.element.querySelector('.e-content') && grid.element.querySelector('.e-content').getAttribute('aria-busy') === 'false') {
// Increase/decrease the pageSet based on the target element.
pageSet = args.target.classList.contains('prevbtn') ? --pageSet : ++pageSet;
this.rerenderGrid(); // Re-render the Grid component.
}
}
You can view the hosted link for this sample here.
If you perform grid actions such as filtering, sorting, etc., after scrolling through the 0.5 million data, the Grid performs those data actions with the whole records, not just the current loaded 0.5 million data.
You can reduce the row height using the rowHeight property of the Grid. It will reduce the overall height to accommodate more rows. But this approach optimizes the limitation, but if the height limit is reached after reducing row height also, you have to opt for the previous solution or use paging.
In the following image, you can see how many records will be scrollable when setting rowHeight to “36px” and “30px”.
Similar to virtual scrolling, the paging feature also loads the data in an on-demand concept. Pagination is also compatible with all the other features(Grouping, Editing, etc.) in Grid. So, use the paging feature instead of virtual scrolling to view a large number of records in the Grid without any kind of performance degradation or browser height limitation.