# Performance Optimization Guide for LWC

> Comprehensive guide to optimizing Lightning Web Component performance, including dark mode implementation, lazy loading, and rendering optimization.

- Skill: `tools-only/performance-optimization-guide-for-lwc-2` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/performance-optimization-guide-for-lwc-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/performance-optimization-guide-for-lwc-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/performance-optimization-guide-for-lwc-2

---

# Performance Optimization Guide for LWC

Comprehensive guide to optimizing Lightning Web Component performance, including dark mode implementation, lazy loading, and rendering optimization.

---

## Table of Contents

1. [Dark Mode Implementation](#dark-mode-implementation)
2. [Rendering Performance](#rendering-performance)
3. [Lazy Loading](#lazy-loading)
4. [Data Management](#data-management)
5. [Event Optimization](#event-optimization)
6. [Memory Management](#memory-management)
7. [Bundle Size Optimization](#bundle-size-optimization)
8. [Performance Testing](#performance-testing)
9. [Common Anti-Patterns](#common-anti-patterns)

---

## Dark Mode Implementation

Dark mode is exclusive to SLDS 2 themes. Components must use global styling hooks to support light/dark theme switching.

### Complete SLDS 2 Color Token Reference

#### Surface Colors

| Token | Light Mode | Dark Mode | Purpose |
|-------|------------|-----------|---------|
| `--slds-g-color-surface-1` | `#FFFFFF` | `#0B0B0B` | Primary surface (body background) |
| `--slds-g-color-surface-2` | `#F3F3F3` | `#181818` | Secondary surface |
| `--slds-g-color-surface-3` | `#E5E5E5` | `#2B2B2B` | Tertiary surface |
| `--slds-g-color-surface-4` | `#C9C9C9` | `#3E3E3E` | Quaternary surface |

#### Container Colors

| Token | Light Mode | Dark Mode | Purpose |
|-------|------------|-----------|---------|
| `--slds-g-color-surface-container-1` | `#FAFAFA` | `#1A1A1A` | Card backgrounds, panels |
| `--slds-g-color-surface-container-2` | `#F7F7F7` | `#232323` | Nested containers |
| `--slds-g-color-surface-container-3` | `#F3F3F3` | `#2E2E2E` | Deep nesting |

#### Text Colors

| Token | Light Mode | Dark Mode | Purpose |
|-------|------------|-----------|---------|
| `--slds-g-color-on-surface` | `#181818` | `#FAFAFA` | Primary text |
| `--slds-g-color-on-surface-1` | `#444444` | `#C9C9C9` | Secondary text |
| `--slds-g-color-on-surface-2` | `#706E6B` | `#A0A0A0` | Muted/disabled text |
| `--slds-g-color-on-surface-inverse` | `#FFFFFF` | `#181818` | Inverse text (buttons, badges) |

#### Border Colors

| Token | Light Mode | Dark Mode | Purpose |
|-------|------------|-----------|---------|
| `--slds-g-color-border-1` | `#C9C9C9` | `#444444` | Primary borders |
| `--slds-g-color-border-2` | `#E5E5E5` | `#3E3E3E` | Secondary borders (dividers) |

#### Brand Colors

| Token | Light Mode | Dark Mode | Purpose |
|-------|------------|-----------|---------|
| `--slds-g-color-brand-1` | `#0176D3` | `#1B96FF` | Primary brand (buttons, links) |
| `--slds-g-color-brand-2` | `#014486` | `#0B5CAB` | Brand hover/active states |

#### Status Colors

| Token | Light Mode | Dark Mode | Purpose |
|-------|------------|-----------|---------|
| `--slds-g-color-success-1` | `#2E844A` | `#45C65A` | Success states |
| `--slds-g-color-error-1` | `#EA001E` | `#FE5C4C` | Error states |
| `--slds-g-color-warning-1` | `#FFB75D` | `#FFB75D` | Warning states |
| `--slds-g-color-info-1` | `#0176D3` | `#1B96FF` | Info states |

#### Spacing Tokens

| Token | Value (rem) | Value (px) |
|-------|-------------|------------|
| `--slds-g-spacing-0` | 0 | 0 |
| `--slds-g-spacing-1` | 0.125 | 2 |
| `--slds-g-spacing-2` | 0.25 | 4 |
| `--slds-g-spacing-3` | 0.5 | 8 |
| `--slds-g-spacing-4` | 0.75 | 12 |
| `--slds-g-spacing-5` | 1 | 16 |
| `--slds-g-spacing-6` | 1.5 | 24 |
| `--slds-g-spacing-7` | 2 | 32 |
| `--slds-g-spacing-8` | 3 | 48 |

### Migration Examples

#### Before: SLDS 1 (Hardcoded Colors)

```css
/* accountCard.css - SLDS 1 (Deprecated) */
.card {
    background-color: #ffffff;
    color: #333333;
    border: 1px solid #dddddd;
    border-radius: 4px;
    padding: 16px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.card-header {
    color: #000000;
    font-size: 16px;
    font-weight: 700;
    margin-bottom: 8px;
}

.card-text {
    color: #666666;
    font-size: 14px;
}

.card-link {
    color: #0176d3;
}

.card-link:hover {
    color: #014486;
    text-decoration: underline;
}
```

#### After: SLDS 2 (Dark Mode Ready)

```css
/* accountCard.css - SLDS 2 */
.card {
    background-color: var(--slds-g-color-surface-container-1, #ffffff);
    color: var(--slds-g-color-on-surface, #181818);
    border: 1px solid var(--slds-g-color-border-2, #e5e5e5);
    border-radius: var(--slds-g-radius-border-2, 0.25rem);
    padding: var(--slds-g-spacing-5, 1rem);
    box-shadow: 0 2px 4px var(--slds-g-color-border-1, rgba(0, 0, 0, 0.1));
}

.card-header {
    color: var(--slds-g-color-on-surface, #181818);
    font-size: var(--slds-g-font-size-5, 1rem);
    font-weight: var(--slds-g-font-weight-bold, 700);
    margin-bottom: var(--slds-g-spacing-3, 0.5rem);
}

.card-text {
    color: var(--slds-g-color-on-surface-1, #444444);
    font-size: var(--slds-g-font-size-3, 0.875rem);
}

.card-link {
    color: var(--slds-g-color-brand-1, #0176d3);
}

.card-link:hover {
    color: var(--slds-g-color-brand-2, #014486);
    text-decoration: underline;
}
```

### Component-Level Example

```javascript
// darkModeCard.js
import { LightningElement } from 'lwc';

export default class DarkModeCard extends LightningElement {
    // No JavaScript changes needed for dark mode!
    // All theming is handled via CSS variables
}
```

```html
<!-- darkModeCard.html -->
<template>
    <div class="card">
        <div class="card-header">
            <h2 class="card-title">Account Details</h2>
        </div>
        <div class="card-body">
            <p class="card-text">This card automatically adapts to light/dark mode</p>
            <a href="#" class="card-link">Learn more</a>
        </div>
    </div>
</template>
```

```css
/* darkModeCard.css */
.card {
    background-color: var(--slds-g-color-surface-container-1, #ffffff);
    border: 1px solid var(--slds-g-color-border-2, #e5e5e5);
    border-radius: var(--slds-g-radius-border-2, 0.25rem);
    padding: var(--slds-g-spacing-5, 1rem);
}

.card-header {
    border-bottom: 1px solid var(--slds-g-color-border-2, #e5e5e5);
    margin-bottom: var(--slds-g-spacing-4, 0.75rem);
    padding-bottom: var(--slds-g-spacing-3, 0.5rem);
}

.card-title {
    color: var(--slds-g-color-on-surface, #181818);
    font-size: var(--slds-g-font-size-5, 1rem);
    font-weight: var(--slds-g-font-weight-bold, 700);
    margin: 0;
}

.card-body {
    color: var(--slds-g-color-on-surface-1, #444444);
}

.card-text {
    margin-bottom: var(--slds-g-spacing-4, 0.75rem);
}

.card-link {
    color: var(--slds-g-color-brand-1, #0176d3);
    text-decoration: none;
}

.card-link:hover {
    color: var(--slds-g-color-brand-2, #014486);
    text-decoration: underline;
}
```

### Validation Script

```bash
# Check for hardcoded colors in CSS
grep -r "#[0-9A-Fa-f]\{3,6\}" force-app/main/default/lwc/ --include="*.css"

# Check for rgb/rgba values
grep -r "rgb\|rgba" force-app/main/default/lwc/ --include="*.css"

# Install SLDS Linter
npm install -g @salesforce-ux/slds-linter

# Run validation
slds-linter lint force-app/main/default/lwc/
```

---

## Rendering Performance

### Conditional Rendering

```javascript
// BAD: Re-renders entire list
<template for:each={allItems} for:item="item">
    <div if:true={item.visible} key={item.id}>
        {item.name}
    </div>
</template>

// GOOD: Filter before rendering
get visibleItems() {
    return this.allItems.filter(item => item.visible);
}

<template for:each={visibleItems} for:item="item">
    <div key={item.id}>{item.name}</div>
</template>
```

### Use `lwc:if` for Large Blocks

```html
<!-- lwc:if removes from DOM (better for large blocks) -->
<template lwc:if={showDashboard}>
    <c-dashboard data={dashboardData}></c-dashboard>
</template>

<!-- if:true hides with CSS (better for frequent toggling) -->
<div if:true={showMessage} class="message">
    {message}
</div>
```

### Key Directive for Lists

```html
<!-- CRITICAL: Use unique, stable keys -->
<template for:each={accounts} for:item="account">
    <div key={account.Id}>  <!-- Use record ID, not index -->
        {account.Name}
    </div>
</template>
```

### Getter Caching

```javascript
// BAD: Recalculates on every render
get formattedValue() {
    return this.expensiveCalculation(this.data);
}

// GOOD: Cache the result
@track _cachedValue;
_cacheKey;

get formattedValue() {
    const currentKey = JSON.stringify(this.data);
    if (this._cacheKey !== currentKey) {
        this._cacheKey = currentKey;
        this._cachedValue = this.expensiveCalculation(this.data);
    }
    return this._cachedValue;
}
```

### Avoid renderedCallback Loops

```javascript
// BAD: Infinite loop
renderedCallback() {
    this.count++; // Triggers re-render
}

// GOOD: Guard against loops
renderedCallback() {
    if (!this._rendered) {
        this._rendered = true;
        this.initializeChart();
    }
}
```

---

## Lazy Loading

### Dynamic Imports

```javascript
// accountManager.js
export default class AccountManager extends LightningElement {
    @track showCharts = false;
    chartModule;

    async handleShowCharts() {
        if (!this.chartModule) {
            // Lazy load chart component
            this.chartModule = await import('c/accountChart');
        }
        this.showCharts = true;
    }
}
```

### Intersection Observer for Lazy Loading

```javascript
// lazyImageLoader.js
export default class LazyImageLoader extends LightningElement {
    @api src;
    @api alt;

    isVisible = false;
    observer;

    renderedCallback() {
        if (!this.observer) {
            this.observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        this.isVisible = true;
                        this.observer.disconnect();
                    }
                });
            }, { rootMargin: '50px' });

            const img = this.template.querySelector('img');
            if (img) {
                this.observer.observe(img);
            }
        }
    }

    get imageSrc() {
        return this.isVisible ? this.src : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
    }

    disconnectedCallback() {
        if (this.observer) {
            this.observer.disconnect();
        }
    }
}
```

```html
<!-- lazyImageLoader.html -->
<template>
    <img src={imageSrc} alt={alt} loading="lazy">
</template>
```

### Virtual Scrolling

```javascript
// virtualList.js
export default class VirtualList extends LightningElement {
    @api items = [];
    @track visibleItems = [];

    itemHeight = 50;
    containerHeight = 500;
    scrollTop = 0;

    get visibleCount() {
        return Math.ceil(this.containerHeight / this.itemHeight);
    }

    get startIndex() {
        return Math.floor(this.scrollTop / this.itemHeight);
    }

    get endIndex() {
        return Math.min(
            this.startIndex + this.visibleCount + 1,
            this.items.length
        );
    }

    get paddingTop() {
        return this.startIndex * this.itemHeight;
    }

    get paddingBottom() {
        return (this.items.length - this.endIndex) * this.itemHeight;
    }

    connectedCallback() {
        this.updateVisibleItems();
    }

    handleScroll(event) {
        this.scrollTop = event.target.scrollTop;
        this.updateVisibleItems();
    }

    updateVisibleItems() {
        this.visibleItems = this.items.slice(
            this.startIndex,
            this.endIndex
        );
    }
}
```

```html
<!-- virtualList.html -->
<template>
    <div class="container"
         style={containerStyle}
         onscroll={handleScroll}>
        <div style={paddingTopStyle}></div>
        <template for:each={visibleItems} for:item="item">
            <div key={item.id} class="item">
                {item.name}
            </div>
        </template>
        <div style={paddingBottomStyle}></div>
    </div>
</template>
```

---

## Data Management

### Debouncing

```javascript
// searchComponent.js
export default class SearchComponent extends LightningElement {
    searchTerm = '';
    delayTimeout;

    handleSearchChange(event) {
        const searchTerm = event.target.value;

        // Clear previous timeout
        clearTimeout(this.delayTimeout);

        // Set new timeout (300ms debounce)
        this.delayTimeout = setTimeout(() => {
            this.performSearch(searchTerm);
        }, 300);
    }

    async performSearch(term) {
        try {
            const results = await searchAccounts({ searchTerm: term });
            this.results = results;
        } catch (error) {
            this.handleError(error);
        }
    }

    disconnectedCallback() {
        clearTimeout(this.delayTimeout);
    }
}
```

### Throttling

```javascript
// scrollTracker.js
export default class ScrollTracker extends LightningElement {
    lastScrollTime = 0;
    throttleDelay = 100;

    handleScroll(event) {
        const now = Date.now();

        if (now - this.lastScrollTime >= this.throttleDelay) {
            this.lastScrollTime = now;
            this.processScroll(event);
        }
    }

    processScroll(event) {
        // Handle scroll logic
        console.log('Scroll position:', event.target.scrollTop);
    }
}
```

### Caching Wire Results

```javascript
// accountList.js
export default class AccountList extends LightningElement {
    @api recordId;
    wiredAccountsResult;

    @wire(getAccounts, { accountId: '$recordId' })
    wiredAccounts(result) {
        this.wiredAccountsResult = result; // Cache for refreshApex
        if (result.data) {
            this.accounts = result.data;
        } else if (result.error) {
            this.error = result.error;
        }
    }

    async handleRefresh() {
        // Refresh cached wire result
        await refreshApex(this.wiredAccountsResult);
    }
}
```

---

## Event Optimization

### Event Delegation

```javascript
// BAD: Multiple event listeners
<template for:each={items} for:item="item">
    <button key={item.id} onclick={handleClick} data-id={item.id}>
        {item.name}
    </button>
</template>

// GOOD: Single delegated listener
<div onclick={handleContainerClick}>
    <template for:each={items} for:item="item">
        <button key={item.id} data-id={item.id}>
            {item.name}
        </button>
    </template>
</div>

handleContainerClick(event) {
    if (event.target.tagName === 'BUTTON') {
        const itemId = event.target.dataset.id;
        this.processClick(itemId);
    }
}
```

### Prevent Event Bubbling

```javascript
handleClick(event) {
    event.stopPropagation(); // Stop bubbling
    event.preventDefault();   // Prevent default action

    // Process event
}
```

---

## Memory Management

### Cleanup in disconnectedCallback

```javascript
export default class ResourceManager extends LightningElement {
    subscription;
    intervalId;
    observer;

    connectedCallback() {
        // Subscribe to events
        this.subscription = subscribe(
            this.messageContext,
            CHANNEL,
            this.handleMessage
        );

        // Set interval
        this.intervalId = setInterval(() => {
            this.updateData();
        }, 5000);

        // Create observer
        this.observer = new IntersectionObserver(
            this.handleIntersection
        );
    }

    disconnectedCallback() {
        // CRITICAL: Clean up all resources
        if (this.subscription) {
            unsubscribe(this.subscription);
            this.subscription = null;
        }

        if (this.intervalId) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }

        if (this.observer) {
            this.observer.disconnect();
            this.observer = null;
        }
    }
}
```

### Remove Event Listeners

```javascript
export default class EventManager extends LightningElement {
    boundHandler;

    connectedCallback() {
        this.boundHandler = this.handleResize.bind(this);
        window.addEventListener('resize', this.boundHandler);
    }

    disconnectedCallback() {
        window.removeEventListener('resize', this.boundHandler);
    }

    handleResize() {
        // Handle resize
    }
}
```

---

## Bundle Size Optimization

### Code Splitting

```javascript
// Import only what you need
import { getRecord } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';

// Don't import entire modules
// BAD: import * as uiRecordApi from 'lightning/uiRecordApi';
```

### Minimize Dependencies

```javascript
// BAD: Import heavy library for simple task
import moment from 'moment';

get formattedDate() {
    return moment(this.date).format('MM/DD/YYYY');
}

// GOOD: Use native APIs
get formattedDate() {
    return new Intl.DateTimeFormat('en-US').format(new Date(this.date));
}
```

---

## Performance Testing

### Chrome DevTools Performance Tab

```javascript
// Add performance marks
export default class PerformanceTracked extends LightningElement {
    connectedCallback() {
        performance.mark('component-start');
        this.initializeComponent();
        performance.mark('component-end');
        performance.measure(
            'component-initialization',
            'component-start',
            'component-end'
        );

        const measure = performance.getEntriesByName('component-initialization')[0];
        console.log('Initialization took:', measure.duration, 'ms');
    }
}
```

### Lighthouse Audit

```bash
lighthouse https://your-org.lightning.force.com --only-categories=performance
```

### Custom Performance Metrics

```javascript
export default class MetricsTracker extends LightningElement {
    connectedCallback() {
        // Track time to interactive
        const startTime = performance.now();

        this.loadData().then(() => {
            const endTime = performance.now();
            console.log('Time to interactive:', endTime - startTime, 'ms');
        });
    }
}
```

---

## Common Anti-Patterns

### 1. Excessive Wire Calls

```javascript
// BAD: Multiple wire calls for related data
@wire(getAccount, { accountId: '$recordId' }) account;
@wire(getContacts, { accountId: '$recordId' }) contacts;
@wire(getOpportunities, { accountId: '$recordId' }) opportunities;

// GOOD: Single wire call with joined data
@wire(getAccountWithRelated, { accountId: '$recordId' })
wiredData({ data, error }) {
    if (data) {
        this.account = data.account;
        this.contacts = data.contacts;
        this.opportunities = data.opportunities;
    }
}
```

### 2. Updating Tracked Properties in Getters

```javascript
// BAD: Side effects in getter
@track count = 0;

get message() {
    this.count++; // Causes infinite re-render!
    return `Count: ${this.count}`;
}

// GOOD: Pure getter
get message() {
    return `Count: ${this.count}`;
}
```

### 3. Not Using @track Wisely

```javascript
// BAD: Over-using @track
@track simpleValue = 'hello';
@track anotherValue = 42;

// GOOD: Only track complex objects
simpleValue = 'hello'; // Primitives don't need @track
@track complexObject = { nested: { value: 42 } };
```

### 4. Heavy Operations in renderedCallback

```javascript
// BAD: Heavy calculation every render
renderedCallback() {
    this.calculateComplexMetrics(); // Expensive!
}

// GOOD: Calculate only when data changes
@track _dataVersion = 0;
_renderedVersion = -1;

renderedCallback() {
    if (this._renderedVersion !== this._dataVersion) {
        this._renderedVersion = this._dataVersion;
        this.calculateComplexMetrics();
    }
}

handleDataChange() {
    this._dataVersion++;
}
```

---

## Performance Checklist

- [ ] Use SLDS 2 color tokens (dark mode ready)
- [ ] Lazy load components with dynamic imports
- [ ] Implement virtual scrolling for long lists (100+ items)
- [ ] Debounce search inputs (300ms)
- [ ] Throttle scroll/resize handlers (100ms)
- [ ] Cache expensive getter calculations
- [ ] Use `lwc:if` for large conditional blocks
- [ ] Provide stable keys in `for:each` loops
- [ ] Clean up resources in `disconnectedCallback()`
- [ ] Avoid heavy operations in `renderedCallback()`
- [ ] Use event delegation for list items
- [ ] Minimize wire service calls
- [ ] Remove unused imports
- [ ] Test with Chrome DevTools Performance tab
- [ ] Run Lighthouse performance audit

---

## Related Resources

- [component-patterns.md](component-patterns.md) - Implementation patterns
- [accessibility-guide.md](accessibility-guide.md) - A11y compliance
- [jest-testing.md](jest-testing.md) - Testing strategies
- [SLDS 2 Transition Guide](https://www.lightningdesignsystem.com/2e1ef8501/p/8184ad-transition-to-slds-2)
- [LWC Performance Best Practices](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.create_performance)

