Skip to main content

How to Implement Infinite Scrolling and Lazy Loading in LWC Datatable

In plain words: Lazy loading (infinite scrolling) in an LWC datatable loads data in small batches as the user scrolls down the table, rather than fetching thousands of records upfront, leading to faster page loads and lower browser memory consumption.

Displaying large datasets in Salesforce can strain browser performance and exceed Apex governor limits. With the standard lightning-datatable component, you can implement infinite loading using the native onloadmore and enable-infinite-loading attributes, delivering a smooth scrolling experience.

Prerequisites

  • A Salesforce Developer Edition, Sandbox, or Scratch Org.
  • Salesforce CLI (sf) installed and authenticated.
  • Basic understanding of imperative Apex calls and asynchronous JavaScript handling in LWC.

Step 1: Set Up the Server-Side Apex Controller

Create an Apex class named AccountTableController.cls. Use SOQL LIMIT and OFFSET clauses to return records in chunks, and check total count to determine when all records have loaded:

public with sharing class AccountTableController {

    public class AccountWrapper {
        @AuraEnabled public List<Account> accounts;
        @AuraEnabled public Integer totalRecords;
    }

    @AuraEnabled(cacheable=true)
    public static AccountWrapper getAccounts(Integer recordLimit, Integer recordOffset) {
        AccountWrapper wrapper = new AccountWrapper();
        wrapper.totalRecords = [SELECT COUNT() FROM Account WITH USER_MODE];
        wrapper.accounts = [
            SELECT Id, Name, Industry, Phone, AnnualRevenue 
            FROM Account 
            WITH USER_MODE 
            ORDER BY Name ASC 
            LIMIT :recordLimit 
            OFFSET :recordOffset
        ];
        return wrapper;
    }
}
Warning Trap: Avoid using wire services for infinite scrolling when updating an existing array in-place. Wire adapters manage their own internal cache and can cause race conditions during array mutations. For infinite loading, use imperative Apex inside the onloadmore handler.

Step 2: Build the LWC Template Markup

In infiniteDataTable.html, place the lightning-datatable inside a container with a fixed height. Bind the onloadmore event and control infinite loading using enable-infinite-loading:

<template>
    <lightning-card title="Account Directory (Infinite Scroll)" icon-name="standard:account">
        <div class="slds-p-around_medium">
            <div style="height: 350px;">
                <lightning-datatable
                    key-field="Id"
                    data={data}
                    columns={columns}
                    enable-infinite-loading={enableInfiniteLoading}
                    onloadmore={handleLoadMore}
                    is-loading={isLoading}
                    hide-checkbox-column>
                </lightning-datatable>
            </div>
            <template lwc:if={loadMoreStatus}>
                <p class="slds-text-align_center slds-text-color_weak slds-m-top_small">
                    {loadMoreStatus}
                </p>
            </template>
        </div>
    </lightning-card>
</template>

Step 3: Implement the JavaScript Controller Logic

In infiniteDataTable.js, make an initial imperative Apex call on load and append additional rows to the data array as the user reaches the bottom:

import { LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccounts from '@salesforce/apex/AccountTableController.getAccounts';

const COLUMNS = [
    { label: 'Account Name', fieldName: 'Name', type: 'text' },
    { label: 'Industry', fieldName: 'Industry', type: 'text' },
    { label: 'Phone', fieldName: 'Phone', type: 'phone' },
    { label: 'Annual Revenue', fieldName: 'AnnualRevenue', type: 'currency' }
];

export default class InfiniteDataTable extends LightningElement {
    columns = COLUMNS;
    data = [];
    recordLimit = 20;
    recordOffset = 0;
    totalRecords = 0;
    isLoading = false;
    enableInfiniteLoading = true;
    loadMoreStatus = '';

    connectedCallback() {
        this.fetchRecords();
    }

    fetchRecords() {
        this.isLoading = true;

        getAccounts({ recordLimit: this.recordLimit, recordOffset: this.recordOffset })
            .then(result => {
                this.totalRecords = result.totalRecords;
                const newRecords = result.accounts;

                // Concatenate new batch to existing dataset
                this.data = [...this.data, ...newRecords];
                this.recordOffset += this.recordLimit;

                // Disable infinite scrolling when all records have been loaded
                if (this.data.length >= this.totalRecords) {
                    this.enableInfiniteLoading = false;
                    this.loadMoreStatus = `All ${this.totalRecords} records loaded.`;
                }
            })
            .catch(error => {
                this.enableInfiniteLoading = false;
                this.showToast('Error', error?.body?.message || 'Failed to load records', 'error');
            })
            .finally(() => {
                this.isLoading = false;
            });
    }

    handleLoadMore(event) {
        if (this.data.length < this.totalRecords) {
            this.fetchRecords();
        } else {
            this.enableInfiniteLoading = false;
            event.target.isLoading = false;
        }
    }

    showToast(title, message, variant) {
        this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
    }
}
360 Architecture Summary:
  • Native Scroll Event: Always rely on onloadmore rather than manual window.onscroll event listeners.
  • Fixed Height Container: A fixed height style (e.g., height: 350px;) on the parent wrapper element is required for the internal scrollbar to trigger properly.
  • SOQL OFFSET Limit: The maximum SOQL OFFSET supported by Salesforce is 2,000. For datasets exceeding 2,000 rows, use keyset pagination (sorting by ID or timestamp) instead of OFFSET.

Step 4: Configure Metadata and Deploy

Update infiniteDataTable.js-meta.xml to make the component available in Lightning App Builder:

<?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>
</LightningComponentBundle>
Deployment Routine:
# Deploy the controller and LWC bundle
sf project deploy start

# Open target org in browser
sf org open
  • In Salesforce Setup, navigate to Lightning App Builder.
  • Drop infiniteDataTable onto your page layout, save, and activate.
  • Scroll to the bottom of the table to verify automated batch retrieval in real time.
Core Takeaway: Pairing the native onloadmore datatable event with paginated imperative Apex calls provides clean, high-performance lazy loading while keeping browser memory and SOQL limits under control.