LearnFrenzy
Sign in Take a free test

Pro Patterns for LWC JavaScript · Lesson 23 of 25

LWC JavaScript Best Practices and Performance

6 min read Updated Nov 8, 2026 Saurabh Samir

Most slow components aren't slow because JavaScript is slow. They're slow because they do the same work over and over: getters that re-sort thousands of rows on every render, Apex calls that could be cached, sections rendered that nobody opened. This lesson collects the habits that keep components fast and code that a teammate can pick up in a year.

In this lesson

  • How often getters really run
  • Render less, load less
  • Server calls: cache, batch, debounce
  • Cleanup and memory
  • Conventions and lint rules

Getters run on every render

A getter used in the template runs each time the component re-renders – for any reason, not just when its own data changes. Let's count:

// leaderboard.js
import { LightningElement } from 'lwc';

export default class Leaderboard extends LightningElement {
    reps = [
        { id: 1, name: 'Asha', won: 12 },
        { id: 2, name: 'Ravi', won: 19 },
        { id: 3, name: 'Neha', won: 15 },
    ];
    showHelp = false;

    get ranked() {
        console.log('sorting reps…');               // imagine 5,000 rows here
        return this.reps.toSorted((a, b) => b.won - a.won);
    }

    toggleHelp() {
        this.showHelp = !this.showHelp;              // unrelated to the ranking
    }
}
<!-- leaderboard.html -->
<template>
    <lightning-button label="Help" onclick={toggleHelp}></lightning-button>
    <template lwc:if={showHelp}><p>Ranked by deals won this quarter.</p></template>
    <template for:each={ranked} for:item="rep">
        <p key={rep.id}>{rep.name} – {rep.won}</p>
    </template>
</template>

Result

[Help]
Ravi – 19
Neha – 15
Asha – 12
console: sorting reps…

› click “Help”
[Help]
Ranked by deals won this quarter.
Ravi – 19
Neha – 15
Asha – 12
console: sorting reps…

› click “Help”
[Help]
Ravi – 19
Neha – 15
Asha – 12
console: sorting reps…

Clicking an unrelated button re-sorted the list twice. For three rows that doesn't matter; for thousands, it's visible lag. The fix: compute when the inputs change and store the result in a field.

// leaderboardFast.js
import { LightningElement } from 'lwc';

export default class LeaderboardFast extends LightningElement {
    ranked = [];
    showHelp = false;

    connectedCallback() {
        this.setReps([
            { id: 1, name: 'Asha', won: 12 },
            { id: 2, name: 'Ravi', won: 19 },
            { id: 3, name: 'Neha', won: 15 },
        ]);
    }

    setReps(reps) {                                  // call this whenever the data changes (wire handler, refresh)
        console.log('sorting reps…');
        this.ranked = reps.toSorted((a, b) => b.won - a.won);
    }

    toggleHelp() {
        this.showHelp = !this.showHelp;
    }
}
<!-- leaderboardFast.html -->
<template>
    <lightning-button label="Help" onclick={toggleHelp}></lightning-button>
    <template lwc:if={showHelp}><p>Ranked by deals won this quarter.</p></template>
    <template for:each={ranked} for:item="rep">
        <p key={rep.id}>{rep.name} – {rep.won}</p>
    </template>
</template>

Result

[Help]
Ravi – 19
Neha – 15
Asha – 12
console: sorting reps…

› click “Help”
[Help]
Ranked by deals won this quarter.
Ravi – 19
Neha – 15
Asha – 12

› click “Help”
[Help]
Ravi – 19
Neha – 15
Asha – 12

Cheap getters (a label, a boolean, a CSS class) are perfectly fine – that's what they're for. Move only the expensive work: sorting, filtering, grouping and formatting large lists.

Render less, load less

  • Use lwc:if for things users rarely open. Content inside a false lwc:if isn't created at all; hiding with CSS still builds it.
  • Always use stable keys in for:each – the record Id, never the array index – so LWC can reuse rows instead of rebuilding them.
  • Don't render thousands of rows. Page the data, or use lightning-datatable with enable-infinite-loading and load more on onloadmore.
  • Load heavy parts on demand. Tabs load their content when opened; dynamic components (lwc:component with lwc:is) download a component's code only when needed.
  • Never set state in renderedCallback without a guard – it triggers another render and can loop.

Server calls: cache, batch, debounce

PracticeWhy
Mark read-only Apex @AuraEnabled(cacheable=true)results are cached on the client and can be used with @wire
Prefer Lightning Data Service (getRecord, lightning-record-form) for single recordsshared cache across components, no Apex to maintain
One Apex call returning a wrapper instead of five small callsfewer round trips; each call has overhead
Debounce search and auto-saveone call when the user pauses, not one per keystroke
Query only the fields and rows you showsmaller payloads, faster serialisation

Cleanup and memory

Anything you start, stop in disconnectedCallback: setInterval/setTimeout, window/document listeners, Lightning Message Service and empApi subscriptions, ResizeObserver/IntersectionObserver instances, and third-party library objects (chart.destroy()). Lightning pages are single-page apps: users navigate for hours without a reload, so leaks pile up.

Conventions that age well

  • Names: components in camelCase folders (accountSearch), used as kebab-case tags (c-account-search); handlers named handleSomething; booleans as isLoading, hasErrors, canEdit.
  • Constants in UPPER_SNAKE_CASE at module level: const PAGE_SIZE = 50;
  • Order inside the class: public @api properties, private fields, @wires, lifecycle hooks, getters, handlers, helpers.
  • Small components with one job; shared logic in utility modules; business rules in pure, tested functions.
  • Labels, not literals: user-facing text in custom labels so it can be translated.
  • Lint and format on save: Salesforce projects ship with @salesforce/eslint-config-lwc and Prettier. Treat lint warnings (no-async-operation, no-api-reassignments, no-inner-html) as review comments from a senior developer.

Practice

Try it yourself

  1. A component wires an Apex method that returns 2,000 cases, and a getter filters them by the search term. Typing feels slow. Name two improvements.

    Show answer

    Debounce the search input and compute the filtered list in the handler (store it in a field) instead of in a getter; better still, filter on the server with a LIMIT and paginate so the browser never holds 2,000 rows.

  2. Why is key={index} a problem in a list where users can delete rows?

    Show answer

    After a delete, every row below gets a new index, so LWC thinks they're different items and re-creates them – losing input state and wasting work. Use the record Id.

Quick check

When does a template getter run?

On every render of the component, whatever caused it.

What does cacheable=true give you?

Client-side caching and the ability to use the method with @wire.

Is content inside a false lwc:if created?

No – it isn't rendered at all.

Summary

  • Keep getters cheap; compute expensive results when inputs change.
  • Render only what's needed, with stable keys and paged data.
  • Cache, batch and debounce server calls; prefer LDS for single records.
  • Clean up everything you start; follow naming conventions and lint rules.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links