# 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 ``` ```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 // GOOD: Filter before rendering get visibleItems() { return this.allItems.filter(item => item.visible); } ``` ### Use `lwc:if` for Large Blocks ```html
{message}
``` ### Key Directive for Lists ```html ``` ### 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 ``` ### 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 ``` --- ## 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 // GOOD: Single delegated listener
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 Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide)