| 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:
Example: A custom <data-table> 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.
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 });
}
}
window.addEventListener('unhandledrejection', (e) => {
console.error('Unhandled promise rejection:', e.reason);
if (window.errorTracker) {
window.errorTracker.captureException(e.reason);
}
e.preventDefault();
});
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.
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;
}
}
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.
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;
}
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();
}
}
}
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.
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;
}
}
}
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 = [];
}
}
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() {
}
}
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.
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;
}
setAge(age) {
this.#age = age;
}
getName() {
return this.#name;
}
getEmail() {
return this.#email;
}
getAge() {
return this.#age;
}
}
function createPoint(x = 0, y = 0, z = 0) {
return { x, y, z };
}
const points = Array.from({ length: 1_000 }, (_, i) => createPoint(i, i * 2, i * 3));
let sum = 0;
for (const point of points) {
sum += point.x + point.y + point.z;
}
Rule 5: Prefer Immutability
Immutability prevents bugs from shared mutable state, enables time-travel debugging, and makes code predictable.
function addItemToCart(cart, item) {
return {
...cart,
items: [...cart.items, item],
total: cart.total + item.price
};
}
function updateUserAddress(user, newAddress) {
return {
...user,
profile: {
...user.profile,
address: {
...user.profile.address,
...newAddress
}
}
};
}
const numbers = [5, 2, 8, 1, 9];
const sorted = numbers.toSorted((a, b) => a - b);
const reversed = numbers.toReversed();
const spliced = numbers.toSpliced(1, 2, 99, 100);
const updated = numbers.with(2, 999);
const copy = structuredClone(original);
Rule 6: Design for Cancellation
Users navigate away, components unmount. Cancellation prevents wasted work and race conditions.
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() {
}
}
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.
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 = `
<div role="alert">
<h2>Something went wrong</h2>
<details>
<summary>Error Details</summary>
<pre>${this.#error?.toString() ?? 'Unknown error'}</pre>
</details>
<button>Try again</button>
</div>
`;
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.
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);
if (window.errorTracker) {
window.errorTracker.captureException(errorInfo.error ?? errorInfo.reason, {
extra: errorInfo
});
}
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.
export class UserRepository {
async findById(id) { }
async save(user) { }
async delete(id) { }
}
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 };
}
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.
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.
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 }
});
}
}
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.
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);
});
}
});
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.
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;
}
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);
restoreFetch();
Rule 14: Property-Based Tests
Example-based tests only cover known cases. Property-based tests generate random inputs, finding edge cases.
describe('Array sorting', () => {
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]);
}
}
});
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);
}
});
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.
function debounce(fn, delay) {
let timeoutId = null;
return (...args) => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn(...args);
timeoutId = null;
}, delay);
};
}
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));
function throttle(fn, limit) {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
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.
const start = performance.now();
expensiveOperation();
console.log(`Took ${performance.now() - start}ms`);
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.
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.
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;
}
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.
delete obj.property;
function bad() { console.log(arguments); }
eval(code);
try { return x * 2; } catch (error) { return 0; }
obj.property = undefined;
const good = (...args) => args.reduce((a, b) => a + b, 0);
new Function(code)();
function goodTryCatch(x) {
return calculate(x);
}
function calculate(x) {
return x * 2;
}
try {
const result = goodTryCatch(value);
} catch (error) {
}
function numberToString(num) {
return String(num);
}
function processNumbers(numbers) {
return numbers.map(n => numberToString(n));
}
Rule 20: Prefer requestAnimationFrame
setTimeout/setInterval aren't synchronized with display refresh, causing jank. requestAnimationFrame ensures smooth 60fps animations.
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;
}
}
}
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.
const integers = [1, 2, 3, 4, 5];
const doubles = [1.5, 2.7, 3.14, 4.2];
const mixed = [1, 'string', {}, null];
const holey = [1, 2, , 4, 5];
const worst = [1, , 'string', , {}];
const array1 = new Array(100).fill(0);
const array2 = [];
for (let i = 0; i < 100; i++) array2.push(i);
const arr = [1, 2, 3, 4, 5];
delete arr[2];
const arr2 = [1, 2, 3, 4, 5];
arr2.splice(2, 1);
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.
class ImageProcessor {
#width;
#height;
#data;
constructor(width, height) {
this.#width = width;
this.#height = height;
this.#data = new Uint8ClampedArray(width * height * 4);
}
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;
}
}
}
class ParticleSystem {
#positions;
#velocities;
#count;
constructor(count) {
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;
}
}
}
Quick Reference
Async
Objects
Performance
Common Gotchas
delete obj.prop;
function f() { arguments }
eval(code);
with (obj) {}
const arr = [1, , 3];
const mixed = [1, 'two'];
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.