--- name: javascript-pragmatic-rules description: 30 pragmatic rules for production JavaScript covering async operations, V8 optimization, memory management, testing, error handling, and performance. Use when writing JavaScript, optimizing performance, handling promises, or building production-grade applications. Includes promise rejection handling, V8 hidden classes, memory leak prevention, and structured testing patterns. allowed-tools: Read, Write, Edit, Grep, Glob, Bash, WebFetch, WebSearch --- # JavaScript Pragmatic Rules 30 battle-tested principles for production JavaScript. ## Relationship with Web Components This skill provides best practices for **all JavaScript code**, including web components. When building interactive UI components with the Custom Elements API, combine this skill with `web-components-architecture`: - **This skill (`javascript-pragmatic-rules`)**: HOW to write JavaScript correctly - Async patterns (Rules 1-4) - Error handling (Rules 7-10) - Performance optimization (Rules 16-22) - Testing strategies (Rules 12-15) - **`web-components-architecture` skill**: WHAT pattern to use for components - Component lifecycle (connectedCallback, disconnectedCallback) - Shadow DOM and encapsulation - Attribute-driven state - HandleEvent pattern **Example:** A custom `` component should: - Use `web-components-architecture` for structure (extends HTMLElement, uses Shadow DOM) - Use `javascript-pragmatic-rules` Rule 4 for cleanup (disconnectedCallback removes listeners) - Use `javascript-pragmatic-rules` Rule 17 for memory leak prevention - Use `javascript-pragmatic-rules` Rule 2 for timeout on async data fetching See `web-components-architecture` skill for component-specific patterns. --- ## Async Operations & Promises ### Rule 1: Handle Promise Rejections Unhandled rejections corrupt state and make debugging impossible. ```javascript // ✅ Explicit error handling with context async function fetchUserData(userId) { try { const response = await fetch(`/api/users/${userId}`); if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); return { success: true, data: await response.json() }; } catch (error) { throw new Error(`User fetch failed for ${userId}`, { cause: error }); } } // Global safety net window.addEventListener('unhandledrejection', (e) => { console.error('Unhandled promise rejection:', e.reason); if (window.errorTracker) { window.errorTracker.captureException(e.reason); } e.preventDefault(); }); // Result type pattern async function safeUserFetch(userId) { try { const response = await fetch(`/api/users/${userId}`); if (!response.ok) return { ok: false, error: `HTTP ${response.status}` }; return { ok: true, value: await response.json() }; } catch (error) { return { ok: false, error: error.message }; } } ``` ### Rule 2: Time-Bound Async Operations Network requests can hang indefinitely. Timeouts prevent resource exhaustion. ```javascript // ✅ AbortController for cancellable fetch async function fetchWithTimeout(url, timeoutMs = 5_000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`); } throw error; } } // Advanced: timeout with exponential backoff retry async function fetchWithRetry(url, options = {}) { const timeout = options.timeout ?? 5_000; const maxRetries = options.maxRetries ?? 3; const retryDelay = options.retryDelay ?? 1_000; const retryOn = options.retryOn ?? [408, 429, 500, 502, 503, 504]; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); if (response.ok) return await response.json(); if (!retryOn.includes(response.status)) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } lastError = new Error(`HTTP ${response.status}`); } catch (error) { clearTimeout(timeoutId); lastError = error.name === 'AbortError' ? new Error(`Timeout after ${timeout}ms`) : error; } if (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, retryDelay * (2 ** attempt))); } } throw new Error(`Failed after ${maxRetries} retries`, { cause: lastError }); } ``` ### Rule 3: Limit Concurrent Operations Unbounded concurrency exhausts resources. Controlled concurrency prevents cascading failures. ```javascript // ✅ Batch processing with concurrency limit async function processInBatches(items, batchSize, processor) { const results = []; for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); const batchResults = await Promise.all(batch.map(processor)); results.push(...batchResults); } return results; } // ✅ Promise pool with semaphore class PromisePool { #concurrency; #running = 0; #queue = []; constructor(concurrency) { this.#concurrency = concurrency; } async run(fn) { while (this.#running >= this.#concurrency) { await new Promise(resolve => this.#queue.push(resolve)); } this.#running++; try { return await fn(); } finally { this.#running--; const resolve = this.#queue.shift(); if (resolve) resolve(); } } } // Advanced: rate limiter with token bucket class RateLimiter { #tokensPerSecond; #tokens; #maxTokens; #lastRefill; constructor(tokensPerSecond, burstSize = tokensPerSecond) { this.#tokensPerSecond = tokensPerSecond; this.#tokens = burstSize; this.#maxTokens = burstSize; this.#lastRefill = Date.now(); } #refill() { const now = Date.now(); const elapsed = (now - this.#lastRefill) / 1_000; this.#tokens = Math.min(this.#maxTokens, this.#tokens + elapsed * this.#tokensPerSecond); this.#lastRefill = now; } async acquire() { this.#refill(); if (this.#tokens >= 1) { this.#tokens -= 1; return; } const timeToWait = ((1 - this.#tokens) / this.#tokensPerSecond) * 1_000; await new Promise(resolve => setTimeout(resolve, timeToWait)); this.#refill(); this.#tokens -= 1; } async run(fn) { await this.acquire(); return fn(); } } ``` ### Rule 4: Clean Up Resources Orphaned timers/listeners cause memory leaks. Proper cleanup is essential. ```javascript // ✅ Web Component cleanup class TimerComponent extends HTMLElement { #intervalId = null; connectedCallback() { this.#intervalId = setInterval(() => { this.textContent = Date.now(); }, 1_000); } disconnectedCallback() { if (this.#intervalId) { clearInterval(this.#intervalId); this.#intervalId = null; } } } // ✅ Cleanup registry pattern class CleanupRegistry { #cleanups = []; registerTimeout(callback, delay) { const id = setTimeout(callback, delay); this.#cleanups.push(() => clearTimeout(id)); return id; } registerInterval(callback, delay) { const id = setInterval(callback, delay); this.#cleanups.push(() => clearInterval(id)); return id; } registerListener(element, event, handler, options) { element.addEventListener(event, handler, options); this.#cleanups.push(() => element.removeEventListener(event, handler, options)); } registerCleanup(fn) { this.#cleanups.push(fn); } cleanup() { for (const fn of this.#cleanups) fn(); this.#cleanups = []; } } // Usage in Web Component class MyComponent extends HTMLElement { #cleanup = new CleanupRegistry(); connectedCallback() { this.#cleanup.registerTimeout(() => console.log('Delayed'), 1_000); this.#cleanup.registerListener(window, 'resize', this.#handleResize.bind(this)); } disconnectedCallback() { this.#cleanup.cleanup(); } #handleResize() { // Handle resize } } // Advanced: cancellable polling class Poller { #fn; #interval; #timeoutId = null; #isActive = false; constructor(fn, interval) { this.#fn = fn; this.#interval = interval; } async #poll() { if (!this.#isActive) return; try { await this.#fn(); } catch (error) { console.error(error); } if (this.#isActive) { this.#timeoutId = setTimeout(() => queueMicrotask(() => this.#poll()), this.#interval); } } start() { if (this.#isActive) return; this.#isActive = true; queueMicrotask(() => this.#poll()); } stop() { this.#isActive = false; if (this.#timeoutId) { clearTimeout(this.#timeoutId); this.#timeoutId = null; } } } ``` ## Object Design & Immutability ### Rule 4a: Initialize All Properties V8 creates hidden classes for objects. Adding properties dynamically causes shape transitions, deoptimizing property access. ```javascript // ✅ Initialize all properties upfront class User { #name; #email; #age; constructor(name, email = null, age = null) { this.#name = name; this.#email = email; this.#age = age; } setEmail(email) { this.#email = email; // No shape transition, just value change } setAge(age) { this.#age = age; } getName() { return this.#name; } getEmail() { return this.#email; } getAge() { return this.#age; } } // Factory ensures consistent shapes function createPoint(x = 0, y = 0, z = 0) { return { x, y, z }; } // All points have identical shape const points = Array.from({ length: 1_000 }, (_, i) => createPoint(i, i * 2, i * 3)); // Optimized loop - V8 knows exact shape let sum = 0; for (const point of points) { sum += point.x + point.y + point.z; // Fast property access } ``` ### Rule 5: Prefer Immutability Immutability prevents bugs from shared mutable state, enables time-travel debugging, and makes code predictable. ```javascript // ✅ Create new objects function addItemToCart(cart, item) { return { ...cart, items: [...cart.items, item], total: cart.total + item.price }; } // ✅ Immutable deep update function updateUserAddress(user, newAddress) { return { ...user, profile: { ...user.profile, address: { ...user.profile.address, ...newAddress } } }; } // ES2023 immutable array operations const numbers = [5, 2, 8, 1, 9]; const sorted = numbers.toSorted((a, b) => a - b); // numbers unchanged const reversed = numbers.toReversed(); // numbers unchanged const spliced = numbers.toSpliced(1, 2, 99, 100); // numbers unchanged const updated = numbers.with(2, 999); // numbers unchanged // Deep cloning with structuredClone const copy = structuredClone(original); // Handles dates, maps, sets, etc. ``` ### Rule 6: Design for Cancellation Users navigate away, components unmount. Cancellation prevents wasted work and race conditions. ```javascript // ✅ Cancellable fetch in Web Component class SearchComponent extends HTMLElement { #query = ''; #results = []; #abortController = null; async #search() { if (this.#abortController) this.#abortController.abort(); this.#abortController = new AbortController(); const signal = this.#abortController.signal; try { const response = await fetch(`/api/search?q=${this.#query}`, { signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); this.#results = await response.json(); this.render(); } catch (error) { if (error.name === 'AbortError') { console.log('Search cancelled'); return; } console.error('Search failed:', error); } } disconnectedCallback() { if (this.#abortController) this.#abortController.abort(); } render() { // Render results } } // Generic cancellable async function function makeCancellable(asyncFn) { let isCancelled = false; const controller = new AbortController(); const promise = (async () => { try { const result = await asyncFn(controller.signal); if (isCancelled) throw new Error('Cancelled'); return result; } catch (error) { if (isCancelled || error.name === 'AbortError') throw new Error('Cancelled'); throw error; } })(); return { promise, cancel: () => { isCancelled = true; controller.abort(); } }; } ``` ### Rule 7: Graceful Error Boundaries Unhandled errors crash entire component trees. Error boundaries contain failures and preserve working UI. ```javascript // ✅ Web Component error boundary class ErrorBoundary extends HTMLElement { #hasError = false; #error = null; connectedCallback() { window.addEventListener('error', this.#handleError.bind(this)); window.addEventListener('unhandledrejection', this.#handleRejection.bind(this)); } disconnectedCallback() { window.removeEventListener('error', this.#handleError.bind(this)); window.removeEventListener('unhandledrejection', this.#handleRejection.bind(this)); } #handleError(event) { this.#hasError = true; this.#error = event.error; this.#render(); if (window.errorTracker) { window.errorTracker.captureException(event.error); } event.preventDefault(); } #handleRejection(event) { this.#hasError = true; this.#error = event.reason; this.#render(); event.preventDefault(); } #resetError() { this.#hasError = false; this.#error = null; this.#render(); } #render() { if (this.#hasError) { this.innerHTML = `

Something went wrong

Error Details
${this.#error?.toString() ?? 'Unknown error'}
`; const button = this.querySelector('button'); if (button) button.addEventListener('click', () => this.#resetError()); } } } customElements.define('error-boundary', ErrorBoundary); ``` ## Error Handling & Resilience ### Rule 8: Zero Uncaught Exceptions Uncaught exceptions crash applications and corrupt state. Global handlers ensure all errors are logged. ```javascript // ✅ Comprehensive error handling setup class GlobalErrorHandler { #logger; constructor(options = {}) { this.#logger = options.logger ?? console; this.#setupHandlers(); } #setupHandlers() { window.addEventListener('error', (event) => { this.#handleError({ type: 'error', message: event.message, filename: event.filename, lineno: event.lineno, error: event.error, timestamp: Date.now() }); event.preventDefault(); }); window.addEventListener('unhandledrejection', (event) => { this.#handleError({ type: 'unhandledRejection', reason: event.reason, timestamp: Date.now() }); event.preventDefault(); }); } #handleError(errorInfo) { this.#logger.error('Global error:', errorInfo); // Send to error tracking service if (window.errorTracker) { window.errorTracker.captureException(errorInfo.error ?? errorInfo.reason, { extra: errorInfo }); } // Use keepalive to ensure delivery if (navigator.sendBeacon) { navigator.sendBeacon('/api/errors', JSON.stringify(errorInfo)); } } } ``` ### Rule 9: Small Module Interfaces Large interfaces couple consumers to implementation details. Small, focused modules are easier to test and maintain. ```javascript // ✅ Focused modules (1-3 exports) // user_repository.js - Data access (1 export) export class UserRepository { async findById(id) { /* ... */ } async save(user) { /* ... */ } async delete(id) { /* ... */ } } // user_validator.js - Validation (1 export) export function validateUser(user) { const errors = []; if (!user.email || !isValidEmail(user.email)) errors.push('Invalid email'); if (!user.password || user.password.length < 8) errors.push('Password must be 8+ characters'); return { isValid: errors.length === 0, errors }; } // Facade pattern for clean interface export class OrderService { #payment; #inventory; #shipping; constructor() { this.#payment = new PaymentProcessor(); this.#inventory = new InventoryManager(); this.#shipping = new ShippingCalculator(); } async createOrder(orderData) { const inventoryCheck = await this.#inventory.reserve(orderData.items); if (!inventoryCheck.success) throw new Error('Items not available'); const shippingCost = await this.#shipping.calculate(orderData.address); const payment = await this.#payment.charge(orderData.paymentMethod, orderData.total + shippingCost); return { orderId: payment.orderId, total: orderData.total + shippingCost }; } } ``` ### Rule 10: Map Errors to User Messages Technical error messages confuse users. User-friendly messages improve UX. ```javascript // ✅ Error mapping class ErrorMapper { #errorMap = new Map([ ['NETWORK_ERROR', { title: 'Connection Problem', message: 'Check your internet.', action: 'Retry' }], ['TIMEOUT', { title: 'Request Timeout', message: 'Request took too long. Try again.', action: 'Retry' }], ['UNAUTHORIZED', { title: 'Sign In Required', message: 'Session expired. Sign in again.', action: 'Sign In' }], ['FORBIDDEN', { title: 'Access Denied', message: 'No permission for this action.', action: null }], ['VALIDATION_ERROR', { title: 'Invalid Input', message: 'Check your input and try again.', action: 'Edit' }], ['SERVER_ERROR', { title: 'Server Error', message: 'Something went wrong. Team notified.', action: 'Contact Support' }], ['NOT_FOUND', { title: 'Not Found', message: 'Resource not found.', action: 'Go Back' }] ]); mapError(errorCode, context = {}) { const mapped = this.#errorMap.get(errorCode); if (!mapped) return { title: 'Unexpected Error', message: 'An error occurred. Try again.', action: 'Retry' }; let message = mapped.message; for (const key of Object.keys(context)) { message = message.replaceAll(`{${key}}`, context[key]); } return { ...mapped, message }; } fromHTTPStatus(status) { const statusMap = { 400: 'VALIDATION_ERROR', 401: 'UNAUTHORIZED', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 408: 'TIMEOUT', 429: 'RATE_LIMITED', 500: 'SERVER_ERROR', 502: 'SERVER_ERROR', 503: 'SERVER_ERROR', 504: 'TIMEOUT' }; return this.mapError(statusMap[status] ?? 'SERVER_ERROR'); } } ``` ## Logging & Observability ### Rule 11: Structured Logging Plain text logs are hard to query. Structured logs enable powerful filtering and debugging. ```javascript // ✅ Structured logging with context class Logger { #serviceName; #environment; #minimumLevel; #levels = { debug: 0, info: 1, warn: 2, error: 3 }; constructor(options = {}) { this.#serviceName = options.serviceName ?? 'app'; this.#environment = options.environment ?? 'development'; this.#minimumLevel = options.minimumLevel ?? 'info'; } log(level, message, context = {}) { if (this.#levels[level] < this.#levels[this.#minimumLevel]) return; const logEntry = { timestamp: new Date().toISOString(), level, service: this.#serviceName, environment: this.#environment, message, ...context }; if (this.#environment === 'production' && navigator.sendBeacon) { navigator.sendBeacon('/api/logs', JSON.stringify(logEntry)); } console[level](JSON.stringify(logEntry)); } info(message, context) { this.log('info', message, context); } warn(message, context) { this.log('warn', message, context); } error(message, error, context = {}) { this.log('error', message, { ...context, error: { message: error.message, stack: error.stack, name: error.name } }); } } // Performance logging class PerformanceLogger extends Logger { startTimer(operation) { const timerId = `${operation}_${Date.now()}`; performance.mark(`${timerId}_start`); return timerId; } endTimer(timerId, operation, context = {}) { performance.mark(`${timerId}_end`); try { performance.measure(timerId, `${timerId}_start`, `${timerId}_end`); const measure = performance.getEntriesByName(timerId)[0]; if (measure) { this.info('Operation completed', { operation, duration: measure.duration, ...context }); } performance.clearMarks(`${timerId}_start`); performance.clearMarks(`${timerId}_end`); performance.clearMeasures(timerId); return measure ? measure.duration : null; } catch (error) { this.error('Failed to measure performance', error); return null; } } } ``` ### Rule 12: Table-Driven Tests Repetitive test code is error-prone. Table-driven tests make it easy to add cases and see patterns. ```javascript // ✅ Table-driven tests describe('calculateDiscount', () => { const testCases = [ [100, 1, 0, 'No discount for single item'], [100, 5, 10, '10% discount for 5 items'], [100, 10, 20, '20% discount for 10 items'], [100, 20, 30, '30% discount for 20+ items'], [0, 10, 0, 'Zero price returns zero'], [100, 0, 0, 'Zero quantity returns zero'] ]; for (const [price, quantity, expected, description] of testCases) { test(`${description}: calculateDiscount(${price}, ${quantity}) = ${expected}`, () => { expect(calculateDiscount(price, quantity)).toBe(expected); }); } }); // Complex test cases describe('validateEmail', () => { const testCases = [ ['user@example.com', true], ['user.name@example.com', true], ['user+tag@example.co.uk', true], ['', false], ['invalid', false], ['@example.com', false], ['user@', false] ]; for (const [email, expected] of testCases) { test(`validateEmail("${email}") should return ${expected}`, () => { expect(validateEmail(email)).toBe(expected); }); } }); ``` ## Testing Strategy ### Rule 13: Realistic API Testing Unit tests with mocks don't catch integration issues. Realistic API tests provide confidence. ```javascript // ✅ Browser-based fetch mocking const originalFetch = window.fetch; function mockFetch(handlers) { window.fetch = async (url, options = {}) => { for (const handler of handlers) { const match = url.match(handler.pattern); if (match && options.method === handler.method) { const response = await handler.handler(match, options.body); return new Response(JSON.stringify(response.body), { status: response.status, headers: { 'Content-Type': 'application/json' } }); } } return originalFetch(url, options); }; } function restoreFetch() { window.fetch = originalFetch; } // Usage const handlers = [ { method: 'GET', pattern: /\/api\/users\/(\d+)/, handler: async (match) => { const id = match[1]; if (id === '999') return { status: 404, body: { error: 'User not found' } }; return { status: 200, body: { id, name: 'Test User', email: 'test@example.com' } }; } } ]; mockFetch(handlers); // Run tests restoreFetch(); ``` ### Rule 14: Property-Based Tests Example-based tests only cover known cases. Property-based tests generate random inputs, finding edge cases. ```javascript // ✅ Property-based testing describe('Array sorting', () => { // Property: sorted array should be in ascending order test('sort produces ascending order', () => { for (let i = 0; i < 100; i++) { const arr = generateRandomArray(); const sorted = arr.toSorted((a, b) => a - b); for (let j = 1; j < sorted.length; j++) { expect(sorted[j]).toBeGreaterThanOrEqual(sorted[j - 1]); } } }); // Property: sorted array contains same elements test('sort preserves all elements', () => { for (let i = 0; i < 100; i++) { const arr = generateRandomArray(); const sorted = arr.toSorted((a, b) => a - b); expect(sorted).toHaveLength(arr.length); for (const item of sorted) expect(arr.includes(item)).toBe(true); } }); // Property: sorting twice gives same result (idempotence) test('sort is idempotent', () => { for (let i = 0; i < 100; i++) { const arr = generateRandomArray(); const sorted1 = arr.toSorted((a, b) => a - b); const sorted2 = sorted1.toSorted((a, b) => a - b); expect(sorted1).toEqual(sorted2); } }); }); function generateRandomArray() { const length = Math.floor(Math.random() * 100); return Array.from({ length }, () => Math.floor(Math.random() * 1_000) - 500); } ``` ### Rule 15: Debounce/Throttle UI Events High-frequency events can fire hundreds of times per second, causing performance issues. ```javascript // ✅ Debounce implementation function debounce(fn, delay) { let timeoutId = null; return (...args) => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => { fn(...args); timeoutId = null; }, delay); }; } // Usage const searchInput = document.getElementById('search'); const debouncedSearch = debounce(async (query) => { const response = await fetch(`/api/search?q=${query}`); const results = await response.json(); displayResults(results); }, 300); searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value)); // ✅ Throttle implementation function throttle(fn, limit) { let inThrottle = false; return (...args) => { if (!inThrottle) { fn(...args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // Usage const throttledScroll = throttle(() => { const scrollY = window.scrollY; document.getElementById('back-to-top').style.display = scrollY > 500 ? 'block' : 'none'; }, 100); window.addEventListener('scroll', throttledScroll); ``` ## Performance Optimization ### Rule 16: Profile Before Optimizing Premature optimization wastes time. Profiling identifies actual bottlenecks. ```javascript // ✅ Performance API const start = performance.now(); expensiveOperation(); console.log(`Took ${performance.now() - start}ms`); // Comprehensive performance monitoring class PerformanceMonitor { #name; constructor(name) { this.#name = name; } start(label = 'default') { performance.mark(`${this.#name}_${label}_start`); } end(label = 'default') { const startMark = `${this.#name}_${label}_start`; const endMark = `${this.#name}_${label}_end`; performance.mark(endMark); performance.measure(`${this.#name}_${label}`, startMark, endMark); const measure = performance.getEntriesByName(`${this.#name}_${label}`)[0]; const duration = measure.duration; if (duration > 100) console.warn(`Slow: ${this.#name}_${label} took ${duration.toFixed(2)}ms`); performance.clearMarks(startMark); performance.clearMarks(endMark); performance.clearMeasures(`${this.#name}_${label}`); return duration; } } ``` ### Rule 17: Avoid Memory Leaks Memory leaks degrade performance over time. Proper cleanup is essential. ```javascript // ✅ Web Component comprehensive cleanup class DataTable extends HTMLElement { #cleanup = []; #intervalId = null; #observer = null; connectedCallback() { const handleResize = () => this.#updateLayout(); window.addEventListener('resize', handleResize); this.#cleanup.push(() => window.removeEventListener('resize', handleResize)); this.#observer = new IntersectionObserver((entries) => this.#handleIntersection(entries)); this.#observer.observe(this); this.#cleanup.push(() => this.#observer.disconnect()); this.#intervalId = setInterval(() => this.#fetchUpdates(), 5_000); this.#cleanup.push(() => clearInterval(this.#intervalId)); } disconnectedCallback() { for (const fn of this.#cleanup) fn(); this.#cleanup = []; } #updateLayout() {} #handleIntersection(entries) {} #fetchUpdates() {} } ``` ### Rule 18: Use Web Workers for CPU Work Heavy computation blocks the main thread. Web Workers enable true parallelism. ```javascript // ✅ Offload computation to worker // worker.js self.onmessage = (event) => { const { type, data } = event.data; if (type === 'PROCESS_DATA') { const result = processLargeDataset(data); self.postMessage({ type: 'RESULT', result }); } }; function processLargeDataset(data) { return data.map(item => { let result = item; for (let i = 0; i < 1_000; i++) result = transform(result); return result; }); } function transform(value) { return value * 2 + 1; } // main.js class WorkerPool { #workers = []; #queue = []; #activeJobs = new Map(); constructor(workerPath, poolSize = navigator.hardwareConcurrency ?? 4) { for (let i = 0; i < poolSize; i++) { const worker = new Worker(workerPath); worker.onmessage = (event) => this.#handleMessage(worker, event); worker.onerror = (error) => this.#handleError(worker, error); this.#workers.push({ worker, isBusy: false }); } } async execute(type, data) { return new Promise((resolve, reject) => { const job = { type, data, resolve, reject }; this.#queue.push(job); this.#processQueue(); }); } #processQueue() { if (this.#queue.length === 0) return; const availableWorker = this.#workers.find(w => !w.isBusy); if (!availableWorker) return; const job = this.#queue.shift(); availableWorker.isBusy = true; this.#activeJobs.set(availableWorker.worker, job); availableWorker.worker.postMessage({ type: job.type, data: job.data }); } #handleMessage(worker, event) { const job = this.#activeJobs.get(worker); if (!job) return; const workerInfo = this.#workers.find(w => w.worker === worker); if (workerInfo) workerInfo.isBusy = false; this.#activeJobs.delete(worker); job.resolve(event.data); this.#processQueue(); } #handleError(worker, error) { const job = this.#activeJobs.get(worker); if (job) { job.reject(error); this.#activeJobs.delete(worker); } const workerInfo = this.#workers.find(w => w.worker === worker); if (workerInfo) workerInfo.isBusy = false; this.#processQueue(); } } ``` ### Rule 19: Avoid Deoptimization Triggers V8 optimizes hot functions with TurboFan JIT. Certain patterns trigger deoptimization. ```javascript // ❌ Triggers deoptimization delete obj.property; function bad() { console.log(arguments); } eval(code); try { return x * 2; } catch (error) { return 0; } // ✅ Optimization-friendly obj.property = undefined; // Set to undefined instead of delete const good = (...args) => args.reduce((a, b) => a + b, 0); // Rest params instead of arguments new Function(code)(); // Function constructor instead of eval // Move try-catch out of hot path function goodTryCatch(x) { return calculate(x); // Hot path is optimizable } function calculate(x) { return x * 2; // Can be inlined and optimized } // Wrap in try-catch at call site only if needed try { const result = goodTryCatch(value); } catch (error) { // Handle error } // Keep call sites monomorphic (single type) function numberToString(num) { return String(num); // Always called with numbers } function processNumbers(numbers) { return numbers.map(n => numberToString(n)); // Monomorphic } ``` ### Rule 20: Prefer requestAnimationFrame setTimeout/setInterval aren't synchronized with display refresh, causing jank. requestAnimationFrame ensures smooth 60fps animations. ```javascript // ✅ Smooth animation class Animator { #rafId = null; #isRunning = false; animate(callback) { if (this.#isRunning) return; this.#isRunning = true; let lastTime = performance.now(); const loop = (currentTime) => { if (!this.#isRunning) return; const deltaTime = currentTime - lastTime; lastTime = currentTime; callback(deltaTime, currentTime); this.#rafId = requestAnimationFrame(loop); }; this.#rafId = requestAnimationFrame(loop); } stop() { this.#isRunning = false; if (this.#rafId) { cancelAnimationFrame(this.#rafId); this.#rafId = null; } } } // Usage: smooth scroll animation function smoothScroll(targetY, duration = 1_000) { const startY = window.scrollY; const distance = targetY - startY; const startTime = performance.now(); const animator = new Animator(); animator.animate((delta, currentTime) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const eased = easeInOutCubic(progress); window.scrollTo(0, startY + distance * eased); if (progress >= 1) animator.stop(); }); } function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - ((-2 * t + 2) ** 3) / 2; } ``` ### Rule 21: Keep Array Types Consistent V8 uses specialized array representations. Mixing types downgrades arrays to slower generic mode. ```javascript // ✅ PACKED_SMI_ELEMENTS (Fastest) const integers = [1, 2, 3, 4, 5]; // All small integers (31-bit), no holes // ✅ PACKED_DOUBLE_ELEMENTS (Fast) const doubles = [1.5, 2.7, 3.14, 4.2]; // All doubles, no holes // ⚠️ PACKED_ELEMENTS (Slower) const mixed = [1, 'string', {}, null]; // Mixed types // ❌ HOLEY_SMI_ELEMENTS (Slow) const holey = [1, 2, , 4, 5]; // Has holes (empty slots) // ❌ HOLEY_ELEMENTS (Slowest) const worst = [1, , 'string', , {}]; // Mixed types AND holes // ✅ Avoid holes const array1 = new Array(100).fill(0); // PACKED_SMI const array2 = []; for (let i = 0; i < 100; i++) array2.push(i); // PACKED_SMI // ❌ Delete creates holes const arr = [1, 2, 3, 4, 5]; delete arr[2]; // Now HOLEY // ✅ Splice to remove const arr2 = [1, 2, 3, 4, 5]; arr2.splice(2, 1); // Still PACKED ``` ### Rule 22: Use Typed Arrays for Numerics Typed arrays provide native memory layout, enabling SIMD operations and cache-friendly access. 10-100x faster than regular arrays for numeric computation. ```javascript // ✅ Image processing class ImageProcessor { #width; #height; #data; constructor(width, height) { this.#width = width; this.#height = height; this.#data = new Uint8ClampedArray(width * height * 4); // RGBA: 4 bytes per pixel } getPixel(x, y) { const index = (y * this.#width + x) * 4; return { r: this.#data[index], g: this.#data[index + 1], b: this.#data[index + 2], a: this.#data[index + 3] }; } setPixel(x, y, r, g, b, a = 255) { const index = (y * this.#width + x) * 4; this.#data[index] = r; this.#data[index + 1] = g; this.#data[index + 2] = b; this.#data[index + 3] = a; } toGrayscale() { for (let i = 0; i < this.#data.length; i += 4) { const gray = this.#data[i] * 0.299 + this.#data[i + 1] * 0.587 + this.#data[i + 2] * 0.114; this.#data[i] = this.#data[i + 1] = this.#data[i + 2] = gray; } } } // Physics simulation class ParticleSystem { #positions; // x, y, z #velocities; #count; constructor(count) { // Structure of Arrays (SoA) for cache efficiency this.#positions = new Float64Array(count * 3); this.#velocities = new Float64Array(count * 3); this.#count = count; } update(deltaTime) { for (let i = 0; i < this.#count; i++) { const idx = i * 3; this.#positions[idx] += this.#velocities[idx] * deltaTime; this.#positions[idx + 1] += this.#velocities[idx + 1] * deltaTime; this.#positions[idx + 2] += this.#velocities[idx + 2] * deltaTime; this.#velocities[idx + 1] -= 9.8 * deltaTime; // Gravity } } } ``` ## Quick Reference ### Async ```javascript // Timeout: await fetchWithTimeout(url, 5_000) // Pool: const pool = new PromisePool(10); await pool.run(fn) // Cleanup: disconnectedCallback() { clearInterval(this.intervalId); } ``` ### Objects ```javascript // Init all: constructor() { this.#prop1 = val1; this.#prop2 = val2; } // Immutable: const next = { ...state, updated: true } // Cancel: const controller = new AbortController(); () => controller.abort() ``` ### Performance ```javascript // Profile: const start = performance.now(); work(); console.log(performance.now() - start) // Typed array: const buffer = new Float64Array(1_000) // Monomorphic: Keep single type at each call site ``` ### Common Gotchas ```javascript // ❌ Don't delete obj.prop; // Use obj.prop = undefined function f() { arguments } // Use rest params eval(code); // Use Function() with (obj) {} // Use destructuring const arr = [1, , 3]; // No holes const mixed = [1, 'two']; // Keep types consistent // ✅ Do obj.prop = undefined; const f = (...args) => {}; new Function(code)(); const { value } = obj; const arr = [1, 0, 3]; const numbers = [1, 2, 3]; ``` ### Priority Matrix **Always Apply:** - Handle rejections (Rule 1) - Timeout async (Rule 2) - Clean up resources (Rule 4) - Init properties (Rule 4a) - Global errors (Rule 8) **Hot Paths Only (>10k ops/sec):** - V8 optimization (Rules 19-22) - Web Workers (Rule 18) - Array optimization (Rule 21-22) **As Needed:** - Concurrency limits (Rule 3) - Immutability (Rule 5) - Cancellation (Rule 6) - Error boundaries (Rule 7) --- For V8 optimization deep dive (Rules 22a-27), see V8_OPTIMIZATION.md.