Implement infinite scrolling for optimized data loading in Lightning Datatable
Lazy loading, also known as infinite scrolling, is a powerful optimization technique that revolutionizes how we handle large datasets in Salesforce. Instead of overwhelming users with thousands of records at once, lazy loading intelligently loads data in smaller chunks only when needed—specifically when users scroll to the bottom of the table.
Lazy loading is an optimization strategy that loads content on-demand rather than all at once. In the context of Lightning Datatables, this means:
Implementing lazy loading in your Lightning Web Components provides several advantages:
Let's build a complete lazy loading solution step by step. We'll create a Lightning Datatable that displays Account records with infinite scrolling capability.
First, we need an Apex class to fetch records with pagination support using LIMIT and OFFSET.
public with sharing class LazyLoadingController {
@AuraEnabled(cacheable=true)
public static List<Account> getAccounts(Integer limitSize, Integer offset){
try {
List<Account> accountList = [
SELECT Id, Name, Industry, Type, Phone, AnnualRevenue
FROM Account
ORDER BY CreatedDate DESC
LIMIT :limitSize
OFFSET :offset
];
return accountList;
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
}
@AuraEnabled(cacheable=true): Makes the method accessible from LWC and enables caching for better performancelimitSize: Number of records to fetch per request (e.g., 25)offset: Starting position for the query (e.g., 0, 25, 50...)ORDER BY CreatedDate DESC: Ensures consistent ordering across requestsThe HTML template uses the lightning-datatable with infinite loading enabled.
<template>
<lightning-card title="Accounts with Lazy Loading" icon-name="standard:account">
<div class="slds-m-around_medium">
<div style="height: 500px;">
<lightning-datatable
key-field="Id"
data={accounts}
columns={columns}
enable-infinite-loading
onloadmore={loadMoreData}
hide-checkbox-column="true"
show-row-number-column="true"
is-loading={isLoading}
>
</lightning-datatable>
</div>
</div>
</lightning-card>
</template>
enable-infinite-loading: Activates the lazy loading featureonloadmore: Event handler that fires when user scrolls to bottomis-loading: Shows spinner while fetching new datastyle="height: 500px;": Fixed height enables scrolling behaviorThe JavaScript handles data fetching, state management, and infinite scrolling logic.
import { LightningElement, track } from 'lwc';
import getAccounts from '@salesforce/apex/LazyLoadingController.getAccounts';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
// Define table columns
const columns = [
{ label: 'Account Name', fieldName: 'Name', type: 'text' },
{ label: 'Industry', fieldName: 'Industry', type: 'text' },
{ label: 'Type', fieldName: 'Type', type: 'text' },
{ label: 'Phone', fieldName: 'Phone', type: 'phone' },
{
label: 'Annual Revenue',
fieldName: 'AnnualRevenue',
type: 'currency',
typeAttributes: {
currencyCode: 'USD',
minimumFractionDigits: 0
}
}
];
export default class LazyLoadingLWC extends LightningElement {
@track accounts = [];
columns = columns;
rowLimit = 25; // Number of records to load per batch
rowOffSet = 0; // Current offset position
isLoading = false; // Loading state
// Load initial data when component loads
connectedCallback() {
this.loadData();
}
// Fetch data from Apex
loadData() {
this.isLoading = true;
return getAccounts({
limitSize: this.rowLimit,
offset: this.rowOffSet
})
.then(result => {
// Append new records to existing ones
let updatedRecords = [...this.accounts, ...result];
this.accounts = updatedRecords;
this.isLoading = false;
})
.catch(error => {
this.isLoading = false;
this.showToast('Error', error.body.message, 'error');
});
}
// Handle infinite scrolling event
loadMoreData(event) {
const { target } = event;
target.isLoading = true;
// Increment offset for next batch
this.rowOffSet = this.rowOffSet + this.rowLimit;
this.loadData()
.then(() => {
target.isLoading = false;
});
}
// Show toast notification
showToast(title, message, variant) {
const event = new ShowToastEvent({
title: title,
message: message,
variant: variant
});
this.dispatchEvent(event);
}
}
Configure where the component can be used in Salesforce.
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
<masterLabel>Lazy Loading Demo</masterLabel>
<description>Lightning Web Component demonstrating lazy loading with infinite scroll</description>
</LightningComponentBundle>
When the component loads, connectedCallback() fires and calls loadData() with offset = 0, fetching the first 25 records.
The first batch of records appears in the datatable. Users can scroll through these records normally.
When users scroll to the bottom of the table, the onloadmore event fires automatically.
The isLoading property is set to true, displaying a spinner at the bottom of the table.
The offset is incremented by 25 (offset = 25), and loadData() fetches the next batch of records.
New records are appended to the existing accounts array using the spread operator: [...this.accounts, ...result]
Steps 3-6 repeat each time the user scrolls to the bottom, until all records are loaded.
Critical: The OFFSET clause in SOQL has a maximum limit of 2,000 records. This means you cannot use OFFSET to paginate beyond 2,000 records.
Error you'll encounter: "NUMBER_OUTSIDE_VALID_RANGE: Maximum SOQL offset allowed is 2000"
For datasets exceeding 2,000 records, use ID-based pagination instead of OFFSET:
Before we dive into the solution, let's understand why ORDER BY Id ASC is crucial for ID-based pagination:
How Id Comparison Works:
When we use WHERE Id > :lastRecordId, Salesforce compares Ids character by character from left to right:
Why This Works for Pagination:
By ordering records by Id and filtering with WHERE Id > lastRecordId, we create "checkpoints" that allow us to:
Visual Example:
Batch 1 (LIMIT 3):
Id: 001D000000IqhSL → Record 1
Id: 001D000000IqhSM → Record 2
Id: 001D000000Iqhaa → Record 3 (lastRecordId stored)
Batch 2 (WHERE Id > '001D000000Iqhaa' LIMIT 3):
Id: 001D000000Iqhab → Record 4
Id: 001D000000Iqhac → Record 5
Id: 001D000000IqhZZ → Record 6 (lastRecordId stored)
Batch 3 (WHERE Id > '001D000000IqhZZ' LIMIT 3):
... and so on
@AuraEnabled(cacheable=true)
public static List<Account> getAccountsWithIdFilter(Integer limitSize, String lastRecordId){
try {
String query = 'SELECT Id, Name, Industry, Type, Phone, AnnualRevenue ' +
'FROM Account ';
// Add ID filter for pagination beyond first batch
if (String.isNotBlank(lastRecordId)) {
query += 'WHERE Id > :lastRecordId ';
}
query += 'ORDER BY Id ASC LIMIT :limitSize';
return Database.query(query);
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
export default class LazyLoadingLWC extends LightningElement {
@track accounts = [];
columns = columns;
rowLimit = 25;
lastRecordId = null; // Track last record ID instead of offset
isLoading = false;
connectedCallback() {
this.loadData();
}
loadData() {
this.isLoading = true;
return getAccountsWithIdFilter({
limitSize: this.rowLimit,
lastRecordId: this.lastRecordId
})
.then(result => {
if (result.length > 0) {
let updatedRecords = [...this.accounts, ...result];
this.accounts = updatedRecords;
// Store the last record's ID for next batch
// This becomes the "checkpoint" for the next query
this.lastRecordId = result[result.length - 1].Id;
}
this.isLoading = false;
})
.catch(error => {
this.isLoading = false;
this.showToast('Error', error.body.message, 'error');
});
}
loadMoreData(event) {
const { target } = event;
target.isLoading = true;
this.loadData()
.then(() => {
target.isLoading = false;
});
}
}
| Aspect | OFFSET Method | ID-based Method |
|---|---|---|
| Max Records | 2,000 records only | Unlimited (millions possible) |
| Query Pattern | LIMIT 25 OFFSET 50 |
WHERE Id > '...' LIMIT 25 |
| Performance | Slower as offset increases | Consistently fast |
| Complexity | Simple to implement | Slightly more complex |
| Use Case | Small to medium datasets | Large datasets (>2,000 records) |
| State Management | Track offset number | Track last record Id |
💡 Pro Tip: Start with OFFSET-based pagination for simplicity. Switch to ID-based pagination only when you expect your dataset to grow beyond 2,000 records or when you notice performance degradation.
cacheable=true for read-only operations to improve performance.Lazy loading is an essential technique for building performant Lightning Web Components that handle large datasets gracefully. By loading data progressively as users scroll, you create a responsive and efficient user experience while optimizing server resources.
Remember to consider the SOQL OFFSET limitation when working with large datasets and implement ID-based pagination for datasets exceeding 2,000 records. With proper implementation and the best practices outlined in this guide, you can create scalable, production-ready components that delight your users.
Start with the basic implementation provided here, test thoroughly with your actual data volumes, and then enhance with additional features based on your specific requirements.