Total Pageviews

Apex, LWC,

LWC Performance Optimization: 10 Mistakes Developers Should Avoid

Lightning Web Components (LWC) is one of the most powerful ways to build modern user interfaces on Salesforce. But writing an LWC that works is not the same as writing an LWC that performs well.

A component may look perfectly fine when you test it with 10 records. The same component can become slow when users work with thousands of records, multiple components, complex Apex logic, or large amounts of data.

In this article, we will look at 10 common LWC performance mistakes and how Salesforce developers can avoid them.

Quick takeaway: Good LWC performance is not only about JavaScript. It depends on the complete chain: LWC → Apex → SOQL → Database → UI Rendering.

Why LWC Performance Matters

Performance directly affects the user experience of Salesforce applications. A slow component can result in:

  • Slow page loading
  • Unnecessary server calls
  • Excessive SOQL execution
  • Large amounts of JavaScript processing
  • Unnecessary DOM rendering
  • Poor mobile experience
  • Governor limit problems

The goal should not be to make every component complicated. The goal is to make the component simple, efficient and scalable.


1. Calling Apex Too Many Times

One of the most common performance problems is making unnecessary Apex calls.

For example, imagine an LWC that loads Account records, then makes another Apex call for Contacts, another for Opportunities and another for related information.

It may work, but the component can become slow because every server request adds overhead.

Problematic Approach


connectedCallback() {
    this.loadAccounts();
    this.loadContacts();
    this.loadOpportunities();
    this.loadCases();
}

loadAccounts() {
    getAccounts().then(result => {
        this.accounts = result;
    });
}

loadContacts() {
    getContacts().then(result => {
        this.contacts = result;
    });
}

loadOpportunities() {
    getOpportunities().then(result => {
        this.opportunities = result;
    });
}

Instead of making unnecessary requests, consider whether the data can be retrieved efficiently through a single Apex transaction or through appropriate data-access patterns.

Better Approach

Design Apex methods around the actual data required by the component rather than creating a separate server call for every small piece of information.

However, do not create one giant Apex method that returns huge amounts of unrelated data. The goal is to find the right balance.


2. Not Using Cacheable Apex Methods

If your Apex method only reads Salesforce data and does not modify records, you should consider using the appropriate caching mechanism.

For Apex methods used with the wire service, developers commonly use:


@AuraEnabled(cacheable=true)
public static List<Account> getAccounts() {

    return [
        SELECT Id, Name, Industry
        FROM Account
        LIMIT 50
    ];
}

Then in LWC:


import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class AccountList extends LightningElement {

    @wire(getAccounts)
    accounts;
}

Caching can reduce unnecessary server communication and make the UI feel faster.

Important: Do not use cacheable=true for Apex methods that perform DML or modify Salesforce data.

3. Querying More Data Than You Actually Need

Another common mistake happens in Apex. Developers sometimes query many fields simply because they might need them later.

Avoid


SELECT Id, Name, Phone, Email, Website, Industry,
       AnnualRevenue, BillingStreet, BillingCity,
       BillingState, BillingPostalCode,
       Description, Owner.Name
FROM Account

If the UI only displays the Account name and phone number, there is no reason to retrieve all these fields.

Better


SELECT Id, Name, Phone
FROM Account

Returning only the required data reduces the amount of information transferred between Salesforce and the browser.

This becomes especially important when dealing with large datasets.


4. Loading Too Many Records at Once

Imagine an Account table containing 20,000 records. Loading all of them into an LWC is not a good design.

The browser now has to:

  • Receive thousands of records
  • Create JavaScript objects
  • Process the data
  • Render the UI
  • Maintain DOM elements

This can make the application noticeably slow.

Better Solution: Pagination

Load a smaller number of records and allow the user to move between pages.


SELECT Id, Name, Phone
FROM Account
ORDER BY Name
LIMIT 50

For large datasets, consider an appropriate pagination strategy instead of repeatedly loading the entire dataset.

Pagination is especially useful for:

  • Lightning Datatable
  • Search results
  • Customer lists
  • Transaction screens
  • Case management applications

5. Rendering Huge Lists in the DOM

Even if Apex returns data quickly, rendering thousands of HTML elements can still make your component slow.

Avoid


<template for:each={accounts} for:item="account">

    <div key={account.Id}>
        {account.Name}
    </div>

</template>

The problem is not the for:each itself. The problem occurs when the collection contains a very large number of records.

Instead, use pagination, filtering or another appropriate strategy so that only the records required by the user are rendered.


6. Using Getters for Heavy Calculations

Getters are useful in LWC, but developers sometimes put expensive calculations inside them.

Example


get processedAccounts() {

    return this.accounts.map(account => {
        // Complex processing
        // Multiple calculations
        // Additional filtering
        return account;
    });
}

Getters can be evaluated during rendering. If the calculation is expensive, repeating it can affect performance.

Better Approach

Perform expensive processing only when the underlying data changes.


processAccounts() {

    this.processedAccounts =
        this.accounts.map(account => {

            return {
                ...account,
                displayName: account.Name
            };

        });
}

Then call the method when the data is actually received or changed.


7. Unnecessary Reactive Properties

Reactivity is one of the strengths of LWC, but that does not mean every variable needs complex reactive behavior.

Developers should keep component state as simple as possible.

Instead of continuously modifying multiple tracked objects, update the state only when necessary.


this.accounts = result;

For arrays and objects, use immutable-style assignments when appropriate:


this.accounts = [
    ...this.accounts,
    newAccount
];

This makes state changes easier to understand and helps the framework determine what needs to be updated.


8. Forgetting Lazy Loading

Not every component needs to load immediately.

Imagine a Salesforce page containing:

  • Account information
  • Contacts
  • Opportunity dashboard
  • Reports
  • Documents
  • Activity history

Loading everything immediately can increase the initial rendering time.

A better design is to load secondary information when the user actually needs it.

For example:

  • Load Account details first.
  • Load Contacts when the Contacts tab is opened.
  • Load documents when the Documents section is opened.
  • Load expensive reports only when requested.

This approach is commonly called lazy loading.


9. Performing SOQL or DML Inside Loops

This is primarily an Apex performance issue, but it directly affects LWC because the component depends on Apex.

Bad Code


for (Account acc : accounts) {

    List<Contact> contacts = [
        SELECT Id, Name
        FROM Contact
        WHERE AccountId = :acc.Id
    ];
}

This can quickly lead to governor limit problems.

Better Approach

Collect the IDs first and perform one bulk query.


Set<Id> accountIds = new Set<Id>();

for (Account acc : accounts) {
    accountIds.add(acc.Id);
}

List<Contact> contacts = [
    SELECT Id, Name, AccountId
    FROM Contact
    WHERE AccountId IN :accountIds
];

Bulkification is one of the most important skills for Salesforce developers.


10. Ignoring the Browser and Network

Sometimes developers assume that every performance problem is caused by Apex. That is not always true.

The problem could be:

  • Large JavaScript processing
  • Too many DOM elements
  • Large network responses
  • Unnecessary Apex calls
  • Large images
  • Expensive rendering
  • Repeated event handlers

Use your browser's developer tools to investigate the actual problem.

Chrome DevTools

Useful areas include:

  • Network: Check API/Apex requests and response sizes.
  • Performance: Analyze rendering and JavaScript execution.
  • Console: Identify JavaScript errors and warnings.
  • Application: Inspect browser-side application information.

Do not optimize based only on assumptions. Measure first, then optimize.


Bonus: Avoid Unnecessary Console Logs

During development, developers often add many console statements:


console.log('Account data:', this.accounts);
console.log('Selected account:', this.selectedAccount);
console.log('Processed data:', this.processedData);

Console logging is useful while debugging, but unnecessary logging should not remain throughout production code.

Keep debugging code under control and remove logs that are no longer required.


LWC Performance Optimization Checklist

Before deploying an LWC to production, ask yourself:

  • Am I making unnecessary Apex calls?
  • Can this read-only Apex method use caching?
  • Am I querying only the fields I need?
  • Am I loading too many records?
  • Do I need pagination?
  • Am I rendering thousands of DOM elements?
  • Are my getters performing expensive calculations?
  • Can some data be lazy loaded?
  • Is my Apex code bulkified?
  • Have I checked browser performance and network calls?

Final Thoughts

LWC performance optimization is not about writing complicated code. In most cases, it is about avoiding unnecessary work.

A good Salesforce developer should think about performance from the beginning of the development process rather than waiting until users complain that an application is slow.

Remember this simple formula:

Less Data + Fewer Server Calls + Efficient Apex + Smart Rendering = Better LWC Performance

When building your next Lightning Web Component, don't only ask: "Does it work?"

Also ask: "Will it still work efficiently when the user has 10,000 records?"

That question can make a big difference between a demo component and a production-ready Salesforce application.


Frequently Asked Questions (FAQ)

What is LWC performance optimization?

LWC performance optimization means improving Lightning Web Components so they load faster, make fewer unnecessary server requests, process less data and render the UI efficiently.

How can I improve LWC performance?

Use efficient Apex, reduce unnecessary server calls, query only required fields, use appropriate caching, paginate large datasets, avoid excessive DOM rendering and lazy load expensive data.

Does Apex affect LWC performance?

Yes. Slow SOQL queries, excessive Apex calls, non-bulkified code and unnecessarily large responses can directly affect LWC performance.

Should I use pagination in LWC?

Pagination is recommended when a component needs to display a large number of records. Loading thousands of records at once can negatively affect both server and browser performance.

Is @AuraEnabled(cacheable=true) useful for LWC?

Yes. For appropriate read-only Apex methods used with the wire service, caching can reduce unnecessary server communication and improve the perceived performance of the component.

Related Salesforce Articles

  • Salesforce Apex Governor Limits Explained
  • Queueable vs Future vs Batch Apex
  • LWC Pagination with Apex
  • Salesforce REST API Integration
  • Salesforce Apex Exception Handling
  • Salesforce Developer Roadmap 2026–2027

Real-World Example: Optimizing an LWC Account Search

Let's take a practical example. Imagine we have an LWC that displays Salesforce Accounts with a search box. A beginner implementation might work perfectly with 20 or 30 records, but it can become slow when the org contains thousands of Accounts.

❌ Example 1: Poorly Designed LWC

Here is an example of an implementation that can cause performance problems:


import { LightningElement } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class AccountSearch extends LightningElement {

    accounts = [];

    connectedCallback() {
        this.loadAccounts();
    }

    loadAccounts() {

        getAccounts()
            .then(result => {
                this.accounts = result;
            })
            .catch(error => {
                console.error(error);
            });
    }

    handleSearch(event) {

        const searchKey = event.target.value;

        // Calling Apex for every key press
        getAccounts({ searchKey: searchKey })
            .then(result => {
                this.accounts = result;
            });
    }
}

At first glance, this code looks fine. But imagine a user searches for: Salesforce

The browser could trigger an Apex request for:

  • S
  • Sa
  • Sal
  • Sale
  • Sales
  • Salesf
  • Salesfo
  • Salesfor
  • Salesforce

That means the application could make many server requests while the user is typing. This is unnecessary and can make the UI feel slow.

❌ Problems in This Approach

  • Apex is called for every key press.
  • There is no minimum search length.
  • There is no debounce mechanism.
  • The server may return more records than required.
  • The Apex method may not be cacheable.
  • There is no pagination or record limit strategy.

✅ Example 2: Improved Apex Controller

We can improve the server-side implementation by returning only the fields required by the UI and limiting the number of records.


public with sharing class AccountController {

    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts(String searchKey) {

        String searchText = '%' + searchKey + '%';

        return [
            SELECT Id, Name, Industry, Phone
            FROM Account
            WHERE Name LIKE :searchText
            ORDER BY Name
            LIMIT 50
        ];
    }
}

Notice a few important things here:

  • with sharing is used for sharing enforcement.
  • cacheable=true is appropriate because this method only reads data.
  • Only the fields required by the UI are queried.
  • The result is limited to 50 records.
  • The Accounts are sorted by name.

✅ Example 3: Improved LWC

Now let's improve the LWC. Instead of calling Apex for every character, we can wait briefly after the user stops typing. This technique is commonly called debouncing.


import { LightningElement } from 'lwc';

import getAccounts
    from '@salesforce/apex/AccountController.getAccounts';

export default class AccountSearch extends LightningElement {

    accounts = [];
    searchKey = '';
    searchTimeout;

    handleSearch(event) {

        this.searchKey = event.target.value;

        clearTimeout(this.searchTimeout);

        if (this.searchKey.length < 2) {
            this.accounts = [];
            return;
        }

        this.searchTimeout = setTimeout(() => {

            this.searchAccounts();

        }, 400);
    }

    searchAccounts() {

        getAccounts({
            searchKey: this.searchKey
        })
        .then(result => {

            this.accounts = result;

        })
        .catch(error => {

            console.error('Account search error:', error);

        });
    }
}

Now the application waits for 400 milliseconds after the user stops typing before making the server request.

For example, if the user types:


Salesforce

instead of potentially making a request for every character, the component can wait until the user pauses and then make a single search request.


Example HTML


<template>

    <lightning-card title="Account Search">

        <div class="slds-p-around_medium">

            <lightning-input
                type="search"
                label="Search Account"
                placeholder="Enter account name..."
                onchange={handleSearch}>
            </lightning-input>

        </div>

        <template if:true={accounts}>

            <template
                for:each={accounts}
                for:item="account">

                <div
                    key={account.Id}
                    class="slds-p-horizontal_medium
                           slds-p-vertical_x-small">

                    <strong>{account.Name}</strong>

                    <div>
                        {account.Industry}
                    </div>

                    <div>
                        {account.Phone}
                    </div>

                </div>

            </template>

        </template>

    </lightning-card>

</template>

What Did We Improve?

Problem Before After
Apex Calls Every key press Debounced search
Data Returned Potentially large Maximum 50 records
SOQL Fields Could retrieve unnecessary fields Only required fields
Apex Caching Not necessarily enabled cacheable=true
UI Rendering Potentially large dataset Limited result set

Another Simple Example: Don't Load 10,000 Records

Here is another common situation I have seen in Salesforce projects. A developer creates an Apex method like this:


@AuraEnabled(cacheable=true)
public static List<Account> getAllAccounts() {

    return [
        SELECT Id, Name
        FROM Account
    ];
}

This might work in a development org where there are only a few hundred Accounts. But what happens when the production org contains 100,000 Accounts?

The LWC does not suddenly become magically faster because it is running in production. The application still has to deal with the amount of data being requested and processed.

A better approach is to think about how much data the user actually needs. For example:


SELECT Id, Name
FROM Account
ORDER BY Name
LIMIT 50

Then implement pagination, filtering, search, or another suitable data-loading strategy.

Developer Tip: Never design an LWC based only on the amount of data available in your developer org. Always ask yourself how the component will behave when the production org has 10x, 100x, or even 1,000x more data.

Real Production Scenario

Suppose a Salesforce application has a customer search screen used by a call-center team. The org contains more than 500,000 Accounts and Contacts.

A developer initially builds the screen with:

  • All records loaded when the component opens
  • Multiple Apex calls
  • No pagination
  • No search debounce
  • Large SOQL responses
  • Heavy JavaScript processing

The component works during development because the developer org contains only a small amount of data. After deployment, users complain:

"Salesforce is very slow."

But Salesforce itself may not be the actual problem. The application design is generating unnecessary work.

After optimizing the component:

  • Only required records are loaded.
  • Search requests are debounced.
  • SOQL retrieves only required fields.
  • Results are limited.
  • Pagination is introduced.
  • Read-only Apex methods use appropriate caching.
  • Large calculations are removed from frequently evaluated getters.

The result is a much more scalable LWC.


Final Developer Lesson

When building an LWC, don't test only whether the component works. Test how it behaves when the data becomes large.

A component that works with 20 records is not necessarily production-ready. A component that continues to perform well with 20,000 or 200,000 records requires a different level of design thinking.

Think like this:

Don't ask: "Can I load this data?"
Ask: "Does the user actually need all this data right now?"