Lazy Loading in Lightning Web Components

Implement infinite scrolling for optimized data loading in Lightning Datatable

Component Overview

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.

What is Lazy Loading?

Lazy loading is an optimization strategy that loads content on-demand rather than all at once. In the context of Lightning Datatables, this means:

💡 Pro Tip: Lazy loading is particularly effective when dealing with tables that have hundreds or thousands of records, where loading everything upfront would cause performance issues and poor user experience.

Key Benefits

Implementing lazy loading in your Lightning Web Components provides several advantages:

Implementation Steps

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.

1. Create the Apex Controller

First, we need an Apex class to fetch records with pagination support using LIMIT and OFFSET.

LazyLoadingController.apxc

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());
        }
    }
}
Understanding the Code:
  • @AuraEnabled(cacheable=true): Makes the method accessible from LWC and enables caching for better performance
  • limitSize: 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 requests

2. Create the Lightning Web Component HTML

The HTML template uses the lightning-datatable with infinite loading enabled.

lazyLoadingLWC.html

<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>
Key Attributes:
  • enable-infinite-loading: Activates the lazy loading feature
  • onloadmore: Event handler that fires when user scrolls to bottom
  • is-loading: Shows spinner while fetching new data
  • style="height: 500px;": Fixed height enables scrolling behavior

3. Create the JavaScript Controller

The JavaScript handles data fetching, state management, and infinite scrolling logic.

lazyLoadingLWC.js

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);
    }
}

4. Create the Component Metadata

Configure where the component can be used in Salesforce.

lazyLoadingLWC.js-meta.xml

<?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>

How It Works: Step-by-Step Flow

  1. Component Initialization

    When the component loads, connectedCallback() fires and calls loadData() with offset = 0, fetching the first 25 records.

  2. Initial Data Display

    The first batch of records appears in the datatable. Users can scroll through these records normally.

  3. Scroll Detection

    When users scroll to the bottom of the table, the onloadmore event fires automatically.

  4. Loading Indicator

    The isLoading property is set to true, displaying a spinner at the bottom of the table.

  5. Fetch Next Batch

    The offset is incremented by 25 (offset = 25), and loadData() fetches the next batch of records.

  6. Append Data

    New records are appended to the existing accounts array using the spread operator: [...this.accounts, ...result]

  7. Repeat Process

    Steps 3-6 repeat each time the user scrolls to the bottom, until all records are loaded.

Important Considerations

⚠️ SOQL Offset Limitation

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"

Solution for Large Datasets (>2,000 records)

For datasets exceeding 2,000 records, use ID-based pagination instead of OFFSET:

🔍 Understanding ORDER BY Id ASC

Before we dive into the solution, let's understand why ORDER BY Id ASC is crucial for ID-based pagination:

  • What is Id? In Salesforce, every record has a unique 18-character Id (e.g., '001D000000IqhSLIAZ')
  • Alphanumeric Sorting: Ids are sorted alphanumerically, similar to how you'd sort words in a dictionary
  • ASC (Ascending): Records are sorted from smallest to largest Id value (A-Z, then 0-9)
  • Consistent Order: Using Id ensures the same order every time you query, unlike fields that can change

How Id Comparison Works:

When we use WHERE Id > :lastRecordId, Salesforce compares Ids character by character from left to right:

  • Id: '001D000000IqhSL' comes BEFORE '001D000000IqhSM'
  • Id: '001D000000IqhSM' comes BEFORE '001D000000Iqhaa'
  • Capital letters come before lowercase in ASCII ordering

Why This Works for Pagination:

By ordering records by Id and filtering with WHERE Id > lastRecordId, we create "checkpoints" that allow us to:

  1. Start from exactly where we left off in the previous batch
  2. Avoid duplicate records across batches
  3. Bypass the 2,000 OFFSET limitation entirely
  4. Work with datasets of any size (millions of records)

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
      

Modified Apex Method for Large Datasets

@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());
    }
}

Updated JavaScript for ID-based Pagination

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;
            });
    }
}

Key Differences: OFFSET vs ID-based Pagination

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.

Best Practices

Enhancement Ideas

Take Your Implementation Further

When to Use Lazy Loading

Use Lazy Loading When:

Conclusion

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.

🚀 Next Steps: