JavaScript Performance Optimization: From Slow to Lightning Fast
Optimize your JavaScript code for maximum performance. Learn about memory management, efficient algorithms, and browser optimization techniques.
Performance is crucial for modern web applications. Users expect fast, responsive interfaces, and even small delays can significantly impact user experience and business metrics. In this comprehensive guide, we'll explore advanced JavaScript performance optimization techniques that will transform your slow applications into lightning-fast experiences.
Understanding Performance Fundamentals
The Performance Timeline
// Measuring performance with the Performance API
function measurePerformance(name, fn) {
const startTime = performance.now()
const result = fn()
const endTime = performance.now()
const duration = endTime - startTime
console.log(`${name} took ${duration.toFixed(2)} milliseconds`)
// Use Performance Observer for more detailed metrics
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms`)
})
})
observer.observe({ entryTypes: ['measure'] })
performance.mark(`${name}-start`)
performance.mark(`${name}-end`)
performance.measure(name, `${name}-start`, `${name}-end`)
}
return result
}
// Usage
const result = measurePerformance('Array Processing', () => {
return largeArray.map((item) => item * 2).filter((item) => item > 100)
})
Memory Management Basics
// Memory leak detection and prevention
class MemoryTracker {
constructor() {
this.references = new Set()
this.intervals = new Set()
this.listeners = new Map()
}
trackReference(obj, name) {
this.references.add({ obj, name, timestamp: Date.now() })
}
trackInterval(intervalId, name) {
this.intervals.add({ id: intervalId, name })
}
trackListener(element, event, handler, name) {
if (!this.listeners.has(element)) {
this.listeners.set(element, [])
}
this.listeners.get(element).push({ event, handler, name })
}
cleanup() {
// Clear intervals
this.intervals.forEach(({ id, name }) => {
clearInterval(id)
console.log(`Cleared interval: ${name}`)
})
// Remove event listeners
this.listeners.forEach((listeners, element) => {
listeners.forEach(({ event, handler, name }) => {
element.removeEventListener(event, handler)
console.log(`Removed listener: ${name}`)
})
})
// Clear references
this.references.clear()
this.intervals.clear()
this.listeners.clear()
}
getMemoryUsage() {
if ('memory' in performance) {
return {
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit,
}
}
return null
}
}
Algorithm and Data Structure Optimization
Efficient Data Structures
// Using Map vs Object for better performance
class PerformantCache {
constructor(maxSize = 1000) {
this.cache = new Map() // Better than Object for frequent additions/deletions
this.maxSize = maxSize
}
set(key, value) {
// LRU eviction
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key)
// Move to end (most recently used)
this.cache.delete(key)
this.cache.set(key, value)
return value
}
return null
}
}
// Set for O(1) lookups instead of Array.includes()
class FastLookup {
constructor(items = []) {
this.itemSet = new Set(items)
this.itemArray = [...items]
}
has(item) {
return this.itemSet.has(item) // O(1) vs O(n) for array
}
add(item) {
if (!this.itemSet.has(item)) {
this.itemSet.add(item)
this.itemArray.push(item)
}
}
remove(item) {
if (this.itemSet.has(item)) {
this.itemSet.delete(item)
const index = this.itemArray.indexOf(item)
this.itemArray.splice(index, 1)
}
}
toArray() {
return this.itemArray
}
}
// Efficient array operations
class ArrayOptimizer {
// Binary search for sorted arrays
static binarySearch(sortedArray, target) {
let left = 0
let right = sortedArray.length - 1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midValue = sortedArray[mid]
if (midValue === target) return mid
if (midValue < target) left = mid + 1
else right = mid - 1
}
return -1
}
// Efficient array deduplication
static deduplicate(array) {
return [...new Set(array)] // Fastest for primitives
}
// Efficient array flattening
static flatten(array, depth = Infinity) {
return depth > 0
? array.reduce(
(acc, val) =>
acc.concat(Array.isArray(val) ? this.flatten(val, depth - 1) : val),
[],
)
: array.slice()
}
// Chunking large arrays for processing
static chunk(array, size) {
const chunks = []
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size))
}
return chunks
}
}
Optimized Algorithms
// Memoization for expensive computations
function memoize(fn, keyGenerator = (...args) => JSON.stringify(args)) {
const cache = new Map()
return function memoized(...args) {
const key = keyGenerator(...args)
if (cache.has(key)) {
return cache.get(key)
}
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
// Example: Expensive Fibonacci with memoization
const fibonacci = memoize((n) => {
if (n < 2) return n
return fibonacci(n - 1) + fibonacci(n - 2)
})
// Debouncing and throttling for performance
function debounce(func, delay, immediate = false) {
let timeoutId
return function debounced(...args) {
const callNow = immediate && !timeoutId
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
timeoutId = null
if (!immediate) func.apply(this, args)
}, delay)
if (callNow) func.apply(this, args)
}
}
function throttle(func, delay) {
let lastExecuted = 0
let timeoutId
return function throttled(...args) {
const now = Date.now()
if (now - lastExecuted > delay) {
func.apply(this, args)
lastExecuted = now
} else {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
func.apply(this, args)
lastExecuted = Date.now()
}, delay - (now - lastExecuted))
}
}
}
// Efficient string operations
class StringOptimizer {
// Template literal caching
static createTemplate(strings, ...keys) {
return function (data) {
const result = [strings[0]]
keys.forEach((key, i) => {
result.push(data[key], strings[i + 1])
})
return result.join('')
}
}
// Efficient string building
static buildString(parts) {
return parts.join('') // Faster than concatenation
}
// String search optimization
static searchOptimized(text, pattern) {
// Use indexOf for simple searches (native optimization)
if (pattern.length === 1) {
return text.indexOf(pattern)
}
// Use regex for complex patterns
const regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
const match = text.match(regex)
return match ? match.index : -1
}
}
DOM Performance Optimization
Efficient DOM Manipulation
// Virtual DOM-like batching
class DOMBatcher {
constructor() {
this.operations = []
this.scheduled = false
}
batch(operation) {
this.operations.push(operation)
if (!this.scheduled) {
this.scheduled = true
requestAnimationFrame(() => this.flush())
}
}
flush() {
// Group operations by type for efficiency
const reads = []
const writes = []
this.operations.forEach((op) => {
if (op.type === 'read') reads.push(op)
else writes.push(op)
})
// Execute all reads first, then all writes
reads.forEach((op) => op.execute())
writes.forEach((op) => op.execute())
this.operations = []
this.scheduled = false
}
}
// Efficient list rendering with virtual scrolling
class VirtualList {
constructor(container, items, itemHeight, visibleCount) {
this.container = container
this.items = items
this.itemHeight = itemHeight
this.visibleCount = visibleCount
this.scrollTop = 0
this.cache = new Map()
this.init()
}
init() {
this.container.style.height = `${this.visibleCount * this.itemHeight}px`
this.container.style.overflow = 'auto'
this.container.style.position = 'relative'
this.viewport = document.createElement('div')
this.viewport.style.height = `${this.items.length * this.itemHeight}px`
this.viewport.style.position = 'relative'
this.container.appendChild(this.viewport)
this.container.addEventListener('scroll', this.handleScroll.bind(this))
this.render()
}
handleScroll() {
const newScrollTop = this.container.scrollTop
if (Math.abs(newScrollTop - this.scrollTop) > this.itemHeight) {
this.scrollTop = newScrollTop
this.render()
}
}
render() {
const startIndex = Math.floor(this.scrollTop / this.itemHeight)
const endIndex = Math.min(
startIndex + this.visibleCount + 1,
this.items.length,
)
// Clear viewport
this.viewport.innerHTML = ''
// Render visible items
for (let i = startIndex; i < endIndex; i++) {
let element = this.cache.get(i)
if (!element) {
element = this.createItem(this.items[i], i)
this.cache.set(i, element)
}
element.style.position = 'absolute'
element.style.top = `${i * this.itemHeight}px`
element.style.height = `${this.itemHeight}px`
this.viewport.appendChild(element)
}
// Clean up cache
if (this.cache.size > this.visibleCount * 2) {
this.cleanupCache(startIndex, endIndex)
}
}
createItem(data, index) {
const div = document.createElement('div')
div.className = 'virtual-item'
div.textContent = `Item ${index}: ${data.name}`
return div
}
cleanupCache(startIndex, endIndex) {
for (const [index] of this.cache) {
if (
index < startIndex - this.visibleCount ||
index > endIndex + this.visibleCount
) {
this.cache.delete(index)
}
}
}
}
// Intersection Observer for lazy loading
class LazyLoader {
constructor(options = {}) {
this.options = {
threshold: 0.1,
rootMargin: '50px',
...options,
}
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
this.options,
)
this.loadedElements = new WeakSet()
}
observe(elements) {
elements.forEach((element) => {
if (!this.loadedElements.has(element)) {
this.observer.observe(element)
}
})
}
handleIntersection(entries) {
entries.forEach((entry) => {
if (entry.isIntersecting && !this.loadedElements.has(entry.target)) {
this.loadElement(entry.target)
this.loadedElements.add(entry.target)
this.observer.unobserve(entry.target)
}
})
}
loadElement(element) {
// Load image
if (element.dataset.src) {
element.src = element.dataset.src
}
// Load content
if (element.dataset.content) {
element.innerHTML = element.dataset.content
}
// Trigger custom load event
element.dispatchEvent(new CustomEvent('lazyload'))
}
}
Asynchronous Performance
Optimized Async Patterns
// Concurrent processing with controlled concurrency
class ConcurrencyController {
constructor(maxConcurrency = 3) {
this.maxConcurrency = maxConcurrency
this.running = 0
this.queue = []
}
async execute(asyncFunction) {
return new Promise((resolve, reject) => {
this.queue.push({ asyncFunction, resolve, reject })
this.processQueue()
})
}
async processQueue() {
if (this.running >= this.maxConcurrency || this.queue.length === 0) {
return
}
this.running++
const { asyncFunction, resolve, reject } = this.queue.shift()
try {
const result = await asyncFunction()
resolve(result)
} catch (error) {
reject(error)
} finally {
this.running--
this.processQueue()
}
}
}
// Efficient data fetching with caching and deduplication
class DataFetcher {
constructor() {
this.cache = new Map()
this.pendingRequests = new Map()
this.concurrencyController = new ConcurrencyController(5)
}
async fetch(url, options = {}) {
const cacheKey = this.getCacheKey(url, options)
// Return cached result
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey)
if (Date.now() - cached.timestamp < (options.ttl || 300000)) {
return cached.data
}
}
// Return pending request
if (this.pendingRequests.has(cacheKey)) {
return this.pendingRequests.get(cacheKey)
}
// Create new request
const requestPromise = this.concurrencyController.execute(async () => {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
})
this.pendingRequests.set(cacheKey, requestPromise)
try {
const data = await requestPromise
// Cache successful result
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
})
return data
} finally {
this.pendingRequests.delete(cacheKey)
}
}
getCacheKey(url, options) {
return `${url}:${JSON.stringify(options)}`
}
clearCache() {
this.cache.clear()
}
preload(urls) {
return Promise.all(urls.map((url) => this.fetch(url)))
}
}
// Background task processing
class BackgroundProcessor {
constructor() {
this.tasks = []
this.isProcessing = false
}
addTask(task, priority = 0) {
this.tasks.push({ task, priority, id: Date.now() })
this.tasks.sort((a, b) => b.priority - a.priority)
if (!this.isProcessing) {
this.processNextTask()
}
}
async processNextTask() {
if (this.tasks.length === 0) {
this.isProcessing = false
return
}
this.isProcessing = true
const { task } = this.tasks.shift()
try {
await this.executeInIdleTime(task)
} catch (error) {
console.error('Background task failed:', error)
}
// Process next task
this.processNextTask()
}
executeInIdleTime(task) {
return new Promise((resolve) => {
if ('requestIdleCallback' in window) {
requestIdleCallback(async (deadline) => {
try {
await task(deadline)
resolve()
} catch (error) {
console.error('Task execution failed:', error)
resolve()
}
})
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(async () => {
try {
await task({ timeRemaining: () => 16 })
resolve()
} catch (error) {
console.error('Task execution failed:', error)
resolve()
}
}, 0)
}
})
}
}
Memory Optimization
Memory Leak Prevention
// WeakMap-based private data storage
const privateData = new WeakMap()
class MemoryEfficientComponent {
constructor(element) {
privateData.set(this, {
element,
listeners: [],
observers: [],
timers: [],
})
this.init()
}
init() {
const data = privateData.get(this)
// Component initialization
}
addListener(event, handler) {
const data = privateData.get(this)
data.element.addEventListener(event, handler)
data.listeners.push({ event, handler })
}
addObserver(observer) {
const data = privateData.get(this)
data.observers.push(observer)
}
addTimer(timerId) {
const data = privateData.get(this)
data.timers.push(timerId)
}
destroy() {
const data = privateData.get(this)
// Clean up listeners
data.listeners.forEach(({ event, handler }) => {
data.element.removeEventListener(event, handler)
})
// Clean up observers
data.observers.forEach((observer) => {
if (observer.disconnect) observer.disconnect()
})
// Clean up timers
data.timers.forEach((timerId) => {
clearTimeout(timerId)
clearInterval(timerId)
})
// WeakMap automatically cleans up when object is garbage collected
privateData.delete(this)
}
}
// Object pooling for frequently created objects
class ObjectPool {
constructor(createFn, resetFn, maxSize = 100) {
this.createFn = createFn
this.resetFn = resetFn
this.maxSize = maxSize
this.pool = []
}
acquire() {
if (this.pool.length > 0) {
return this.pool.pop()
}
return this.createFn()
}
release(obj) {
if (this.pool.length < this.maxSize) {
this.resetFn(obj)
this.pool.push(obj)
}
}
clear() {
this.pool = []
}
}
// Example: DOM element pool
const elementPool = new ObjectPool(
() => document.createElement('div'),
(element) => {
element.innerHTML = ''
element.className = ''
element.removeAttribute('style')
},
)
// Efficient event delegation with cleanup
class EventDelegator {
constructor(container) {
this.container = container
this.handlers = new Map()
this.boundHandler = this.handleEvent.bind(this)
this.container.addEventListener('click', this.boundHandler)
}
addHandler(selector, handler) {
if (!this.handlers.has(selector)) {
this.handlers.set(selector, [])
}
this.handlers.get(selector).push(handler)
}
removeHandler(selector, handler) {
const handlers = this.handlers.get(selector)
if (handlers) {
const index = handlers.indexOf(handler)
if (index > -1) {
handlers.splice(index, 1)
if (handlers.length === 0) {
this.handlers.delete(selector)
}
}
}
}
handleEvent(event) {
for (const [selector, handlers] of this.handlers) {
if (event.target.matches(selector)) {
handlers.forEach((handler) => handler(event))
}
}
}
destroy() {
this.container.removeEventListener('click', this.boundHandler)
this.handlers.clear()
}
}
Bundle and Code Optimization
Code Splitting and Lazy Loading
// Dynamic imports for code splitting
class ModuleLoader {
constructor() {
this.loadedModules = new Map()
this.loadingPromises = new Map()
}
async loadModule(modulePath) {
// Return cached module
if (this.loadedModules.has(modulePath)) {
return this.loadedModules.get(modulePath)
}
// Return existing loading promise
if (this.loadingPromises.has(modulePath)) {
return this.loadingPromises.get(modulePath)
}
// Start loading
const loadingPromise = this.importModule(modulePath)
this.loadingPromises.set(modulePath, loadingPromise)
try {
const module = await loadingPromise
this.loadedModules.set(modulePath, module)
return module
} finally {
this.loadingPromises.delete(modulePath)
}
}
async importModule(modulePath) {
try {
const module = await import(modulePath)
return module.default || module
} catch (error) {
console.error(`Failed to load module ${modulePath}:`, error)
throw error
}
}
preloadModule(modulePath) {
// Preload without waiting
this.loadModule(modulePath).catch((error) => {
console.warn(`Preload failed for ${modulePath}:`, error)
})
}
unloadModule(modulePath) {
this.loadedModules.delete(modulePath)
}
}
// Feature-based lazy loading
class FeatureLoader {
constructor() {
this.features = new Map()
this.moduleLoader = new ModuleLoader()
}
registerFeature(name, modulePath, condition = () => true) {
this.features.set(name, { modulePath, condition, loaded: false })
}
async loadFeature(name) {
const feature = this.features.get(name)
if (!feature) {
throw new Error(`Feature ${name} not registered`)
}
if (!feature.condition()) {
throw new Error(`Condition not met for feature ${name}`)
}
if (!feature.loaded) {
feature.module = await this.moduleLoader.loadModule(feature.modulePath)
feature.loaded = true
}
return feature.module
}
async loadFeatureOnDemand(name, trigger) {
const loadFeature = async () => {
try {
const module = await this.loadFeature(name)
if (module.init) {
module.init()
}
} catch (error) {
console.error(`Failed to load feature ${name}:`, error)
}
}
if (trigger === 'interaction') {
// Load on first user interaction
const events = ['click', 'keydown', 'touchstart']
const handler = () => {
events.forEach((event) => document.removeEventListener(event, handler))
loadFeature()
}
events.forEach((event) =>
document.addEventListener(event, handler, { once: true }),
)
} else if (trigger === 'idle') {
// Load when browser is idle
if ('requestIdleCallback' in window) {
requestIdleCallback(loadFeature)
} else {
setTimeout(loadFeature, 0)
}
} else if (trigger === 'visible') {
// Load when element becomes visible
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
observer.disconnect()
loadFeature()
}
})
})
const element = document.querySelector(`[data-feature="${name}"]`)
if (element) {
observer.observe(element)
}
}
}
}
Performance Monitoring
Real-time Performance Tracking
// Performance monitor
class PerformanceMonitor {
constructor() {
this.metrics = new Map()
this.observers = []
this.setupObservers()
}
setupObservers() {
// Long task observer
if ('PerformanceObserver' in window) {
const longTaskObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.recordMetric('longTask', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
})
})
})
try {
longTaskObserver.observe({ entryTypes: ['longtask'] })
this.observers.push(longTaskObserver)
} catch (e) {
console.warn('Long task observer not supported')
}
// Layout shift observer
const layoutShiftObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.recordMetric('layoutShift', {
value: entry.value,
hadRecentInput: entry.hadRecentInput,
})
})
})
try {
layoutShiftObserver.observe({ entryTypes: ['layout-shift'] })
this.observers.push(layoutShiftObserver)
} catch (e) {
console.warn('Layout shift observer not supported')
}
}
// Memory monitoring
if ('memory' in performance) {
setInterval(() => {
this.recordMetric('memory', {
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit,
})
}, 5000)
}
}
recordMetric(name, data) {
if (!this.metrics.has(name)) {
this.metrics.set(name, [])
}
const metrics = this.metrics.get(name)
metrics.push({
...data,
timestamp: Date.now(),
})
// Keep only recent metrics
const cutoff = Date.now() - 300000 // 5 minutes
this.metrics.set(
name,
metrics.filter((m) => m.timestamp > cutoff),
)
}
getMetrics(name) {
return this.metrics.get(name) || []
}
getAverageMetric(name, property) {
const metrics = this.getMetrics(name)
if (metrics.length === 0) return 0
const sum = metrics.reduce((acc, metric) => acc + metric[property], 0)
return sum / metrics.length
}
getPerformanceScore() {
const longTaskAvg = this.getAverageMetric('longTask', 'duration')
const layoutShiftSum = this.getMetrics('layoutShift').reduce(
(sum, metric) => sum + metric.value,
0,
)
let score = 100
// Penalize long tasks
if (longTaskAvg > 50) score -= Math.min(30, (longTaskAvg - 50) / 10)
// Penalize layout shifts
if (layoutShiftSum > 0.1)
score -= Math.min(20, (layoutShiftSum - 0.1) * 100)
return Math.max(0, Math.round(score))
}
generateReport() {
return {
score: this.getPerformanceScore(),
longTasks: this.getMetrics('longTask').length,
averageLongTaskDuration: this.getAverageMetric('longTask', 'duration'),
cumulativeLayoutShift: this.getMetrics('layoutShift').reduce(
(sum, metric) => sum + metric.value,
0,
),
memoryUsage: this.getMetrics('memory').slice(-1)[0] || null,
timestamp: new Date().toISOString(),
}
}
destroy() {
this.observers.forEach((observer) => observer.disconnect())
this.metrics.clear()
}
}
// Usage
const monitor = new PerformanceMonitor()
// Check performance periodically
setInterval(() => {
const report = monitor.generateReport()
console.log('Performance Report:', report)
if (report.score < 70) {
console.warn('Performance degradation detected!')
}
}, 30000)
Conclusion
JavaScript performance optimization is a multifaceted discipline that requires understanding of algorithms, memory management, browser APIs, and user experience principles. By implementing these advanced techniques, you can create applications that are not just fast, but consistently performant under various conditions.
Key takeaways:
- Measure performance before optimizing
- Use efficient data structures and algorithms
- Implement proper memory management
- Optimize DOM operations and async code
- Leverage code splitting and lazy loading
- Monitor performance in real-time
- Consider the user experience impact of every optimization
Remember that premature optimization can be counterproductive. Always profile your application, identify bottlenecks, and optimize based on real performance data. The goal is to create applications that feel instant and responsive to your users.
Build lightning-fast applications that users love! ⚡🚀