Chapter 15: Performance and Best Practices
· 13 min read
Chapter 15: Performance and Best Practices
Memory Management and Performance
// Avoid memory leaks
class EventManager {
constructor() {
this.listeners = new Map();
}
addEventListener(element, event, handler) {
// Store references for cleanup
if (!this.listeners.has(element)) {
this.listeners.set(element, new Map());
}
this.listeners.get(element).set(event, handler);
element.addEventListener(event, handler);
}
removeEventListener(element, event) {
const elementListeners = this.listeners.get(element);
if (elementListeners) {
const handler = elementListeners.get(event);
if (handler) {
element.removeEventListener(event, handler);
elementListeners.delete(event);
}
}
}
cleanup() {
// Clean up all listeners
for (const [element, events] of this.listeners) {
for (const [event, handler] of events) {
element.removeEventListener(event, handler);
}
}
this.listeners.clear();
}
}
// Debouncing and throttling
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage examples
const debouncedSearch = debounce((query) => {
console.log(`Searching for: ${query}`);
}, 300);
const throttledScroll = throttle(() => {
console.log('Scroll event handled');
}, 100);
// Efficient array operations
// ❌ Inefficient - creates new arrays
let numbers = [1, 2, 3, 4, 5];
numbers = numbers.filter(n => n > 2).map(n => n * 2).filter(n => n < 10);
// ✅ More efficient - single pass
numbers = [1, 2, 3, 4, 5].reduce((acc, n) => {
if (n > 2) {
const doubled = n * 2;
if (doubled < 10) {
acc.push(doubled);
}
}
return acc;
}, []);
Error Handling Best Practices
// Custom error classes
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
// Comprehensive error handling
async function processUserData(userData) {
try {
// Validation
if (!userData.email) {
throw new ValidationError('Email is required', 'email');
}
if (!userData.email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
// API call with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData),
headers: { 'Content-Type': 'application/json' },
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new NetworkError(
`HTTP ${response.status}: ${response.statusText}`,
response.status
);
}
return await response.json();
} catch (error) {
// Handle different error types
if (error instanceof ValidationError) {
console.error(`Validation error in ${error.field}: ${error.message}`);
// Show user-friendly message
} else if (error instanceof NetworkError) {
console.error(`Network error (${error.statusCode}): ${error.message}`);
// Retry logic or fallback
} else if (error.name === 'AbortError') {
console.error('Request timed out');
// Handle timeout
} else {
console.error('Unexpected error:', error);
// Generic error handling
}
throw error; // Re-throw if calling code needs to handle it
}
}
// Global error handling
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
// Send to error reporting service
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // Prevent default browser behavior
});
Performance Optimization Techniques
DOM Manipulation Optimization
// ❌ Inefficient - multiple DOM operations
function inefficientDOMUpdate(items) {
const container = document.getElementById('container');
// Each iteration triggers reflow/repaint
for (const item of items) {
const div = document.createElement('div');
div.textContent = item.name;
container.appendChild(div); // Expensive!
}
}
// ✅ Efficient - batch DOM operations
function efficientDOMUpdate(items) {
const container = document.getElementById('container');
const fragment = document.createDocumentFragment();
// Build in memory first
for (const item of items) {
const div = document.createElement('div');
div.textContent = item.name;
fragment.appendChild(div);
}
// Single DOM operation
container.appendChild(fragment);
}
// ✅ Even better - use innerHTML for large sets
function veryEfficientDOMUpdate(items) {
const container = document.getElementById('container');
const html = items.map(item => `<div>${item.name}</div>`).join('');
container.innerHTML = html;
}
Efficient Object Property Access
// ❌ Slow - dynamic property access in loop
function slowPropertyAccess(objects, propertyName) {
const results = [];
for (const obj of objects) {
results.push(obj[propertyName]); // Dynamic lookup each time
}
return results;
}
// ✅ Fast - cache property access pattern
function fastPropertyAccess(objects, propertyName) {
// For consistent object shapes, engines can optimize
if (propertyName === 'name') {
return objects.map(obj => obj.name);
} else if (propertyName === 'age') {
return objects.map(obj => obj.age);
}
// Fallback for dynamic properties
return objects.map(obj => obj[propertyName]);
}
// Object shape consistency for V8 optimization
class OptimizedUser {
constructor(name, age, email) {
// Always initialize all properties in same order
this.name = name;
this.age = age;
this.email = email;
// Avoid adding properties later - breaks hidden classes
}
}
Memory-Efficient Array Operations
// ❌ Memory intensive - creates many intermediate arrays
function inefficientProcessing(largeArray) {
return largeArray
.filter(x => x > 10)
.map(x => x * 2)
.filter(x => x < 100)
.map(x => x.toString())
.filter(x => x.length < 3);
}
// ✅ Memory efficient - single pass
function efficientProcessing(largeArray) {
const result = [];
for (const x of largeArray) {
if (x > 10) {
const doubled = x * 2;
if (doubled < 100) {
const str = doubled.toString();
if (str.length < 3) {
result.push(str);
}
}
}
}
return result;
}
// ✅ Generator for lazy evaluation
function* lazyProcessing(largeArray) {
for (const x of largeArray) {
if (x > 10) {
const doubled = x * 2;
if (doubled < 100) {
const str = doubled.toString();
if (str.length < 3) {
yield str;
}
}
}
}
}
// Use only what you need
const first5Results = [...lazyProcessing(largeArray)].slice(0, 5);
Code Organization Best Practices
Module Design Patterns
// ✅ Well-structured module
class DataService {
constructor(apiUrl, options = {}) {
this.apiUrl = apiUrl;
this.timeout = options.timeout ?? 5000;
this.retries = options.retries ?? 3;
this.cache = new Map();
}
async fetch(endpoint, options = {}) {
const url = `${this.apiUrl}/${endpoint}`;
const cacheKey = `${url}:${JSON.stringify(options)}`;
// Check cache first
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const data = await this._fetchWithRetry(url, options);
this.cache.set(cacheKey, data);
return data;
} catch (error) {
console.error(`Failed to fetch ${endpoint}:`, error);
throw error;
}
}
async _fetchWithRetry(url, options, attempt = 1) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (attempt < this.retries) {
const delay = Math.pow(2, attempt - 1) * 1000; // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
return this._fetchWithRetry(url, options, attempt + 1);
}
throw error;
}
}
clearCache() {
this.cache.clear();
}
}
// Usage
const api = new DataService('https://api.example.com', {
timeout: 10000,
retries: 2
});
Functional Programming Patterns
// Pure functions and immutability
const UserUtils = {
// Pure function - no side effects
calculateAge(birthDate) {
const today = new Date();
const birth = new Date(birthDate);
return today.getFullYear() - birth.getFullYear();
},
// Immutable updates
updateUser(user, updates) {
return {
...user,
...updates,
updatedAt: new Date().toISOString()
};
},
// Compose functions
compose(...fns) {
return (value) => fns.reduceRight((acc, fn) => fn(acc), value);
},
// Partial application
partial(fn, ...args1) {
return (...args2) => fn(...args1, ...args2);
}
};
// Usage
const addTax = (rate, amount) => amount * (1 + rate);
const addSalesTax = UserUtils.partial(addTax, 0.08);
const formatCurrency = amount => `$${amount.toFixed(2)}`;
const calculateTotal = UserUtils.compose(formatCurrency, addSalesTax);
console.log(calculateTotal(100)); // "$108.00"
Performance Monitoring and Profiling
// Performance measurement utilities
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
}
time(label) {
this.metrics.set(label, performance.now());
}
timeEnd(label) {
const start = this.metrics.get(label);
if (start) {
const duration = performance.now() - start;
console.log(`${label}: ${duration.toFixed(2)}ms`);
this.metrics.delete(label);
return duration;
}
}
// Measure function execution time
measure(fn, label = fn.name) {
return (...args) => {
this.time(label);
const result = fn(...args);
this.timeEnd(label);
return result;
};
}
// Measure async function execution time
measureAsync(fn, label = fn.name) {
return async (...args) => {
this.time(label);
try {
const result = await fn(...args);
this.timeEnd(label);
return result;
} catch (error) {
this.timeEnd(label);
throw error;
}
};
}
// Memory usage snapshot
getMemoryUsage() {
if (performance.memory) {
return {
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
};
}
return null;
}
}
// Usage
const monitor = new PerformanceMonitor();
const expensiveOperation = monitor.measure((data) => {
return data.map(x => x * x).filter(x => x > 100);
});
const result = expensiveOperation([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
console.log('Memory usage:', monitor.getMemoryUsage());
Security Best Practices
// Input sanitization and validation
class InputValidator {
static sanitizeString(input, maxLength = 255) {
if (typeof input !== 'string') return '';
return input
.trim()
.slice(0, maxLength)
.replace(/[<>]/g, ''); // Basic HTML tag removal
}
static validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static validateURL(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
static escapeHTML(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
}
// Secure API calls
class SecureAPI {
constructor(baseURL, token) {
this.baseURL = baseURL;
this.token = token;
}
async request(endpoint, options = {}) {
const url = new URL(endpoint, this.baseURL);
// Validate URL
if (!InputValidator.validateURL(url.toString())) {
throw new Error('Invalid URL');
}
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
'X-Requested-With': 'XMLHttpRequest', // CSRF protection
...options.headers
};
try {
const response = await fetch(url, {
...options,
headers,
credentials: 'same-origin' // Prevent CSRF
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
}
Next Chapter: Chapter 16: Testing and Debugging
Previous Chapter: Chapter 14: Modern JavaScript Features (ES6+)
Table of Contents: JavaScript Guide