We are still cooking the magic in the way!
JavaScript Performance Optimization
JavaScript Performance Optimization
JavaScript performance directly impacts user experience, load times, and interactivity. Modern web applications often ship large JavaScript bundles that can slow down page load and execution. This lesson covers techniques to optimize JavaScript performance through bundle optimization, code splitting, and efficient execution patterns.
Bundle Size Optimization
Reducing JavaScript bundle size improves download times and parsing performance:
<script src="/js/app.bundle.js"></script> <!-- 2.5 MB -->
<!-- After: Optimized chunks -->
<script src="/js/vendor.js"></script> <!-- 500 KB -->
<script src="/js/app.js"></script> <!-- 300 KB -->
<script src="/js/lazy-features.js" defer></script> <!-- 200 KB -->
Tree Shaking
Tree shaking eliminates unused code from your bundles. It works with ES6 modules:
import _ from 'lodash'; // 70 KB
const result = _.debounce(fn, 300);
// Good: Import only what you need
import debounce from 'lodash/debounce'; // 2 KB
const result = debounce(fn, 300);
// Best: Use ES6 module imports
import { debounce } from 'lodash-es'; // Tree-shakeable
// Webpack config for tree shaking
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true
}
};
Code Splitting
Split code into smaller chunks that load on demand:
import React, { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Dynamic imports (Vanilla JS)
document.querySelector('#btn').addEventListener('click', async () => {
const module = await import('./heavy-feature.js');
module.initialize();
});
Lazy Loading
Defer loading of non-critical JavaScript until needed:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
import('./chart-library.js').then(module => {
module.renderChart(entry.target);
});
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('.chart-container').forEach(el => {
observer.observe(el);
});
// Lazy load scripts with attributes
const script = document.createElement('script');
script.src = 'analytics.js';
script.async = true;
script.defer = true;
document.body.appendChild(script);
Web Workers
Offload heavy computations to background threads:
self.addEventListener('message', (e) => {
const data = e.data;
// Heavy computation
const result = processLargeDataset(data);
self.postMessage(result);
});
function processLargeDataset(data) {
// Complex calculations that would block UI
return data.map(item => expensiveOperation(item));
}
// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeDataset);
worker.addEventListener('message', (e) => {
const result = e.data;
updateUI(result);
});
// Clean up when done
worker.terminate();
requestAnimationFrame
Optimize animations and visual updates:
function moveElement() {
element.style.left = position + 'px';
position += 5;
setTimeout(moveElement, 16); // ~60fps but imprecise
}
// Good: Use requestAnimationFrame
function moveElement() {
element.style.left = position + 'px';
position += 5;
if (position < targetPosition) {
requestAnimationFrame(moveElement);
}
}
requestAnimationFrame(moveElement);
// Batching DOM reads and writes
function optimizedLayout() {
// Batch reads
const height1 = el1.offsetHeight;
const height2 = el2.offsetHeight;
// Batch writes
requestAnimationFrame(() => {
el1.style.height = height2 + 'px';
el2.style.height = height1 + 'px';
});
}
Memory Leaks Prevention
Identify and prevent common memory leak patterns:
class Component {
constructor() {
this.handleClick = () => console.log('clicked');
document.addEventListener('click', this.handleClick);
}
destroy() {
// Forgot to remove listener - LEAK!
}
}
// Fixed: Clean up listeners
class Component {
constructor() {
this.handleClick = () => console.log('clicked');
document.addEventListener('click', this.handleClick);
}
destroy() {
document.removeEventListener('click', this.handleClick);
}
}
// Leak: Timers not cleared
const intervalId = setInterval(() => {
updateData();
}, 1000);
// Component unmounts but interval continues - LEAK!
// Fixed: Clear timers
const intervalId = setInterval(() => updateData(), 1000);
window.addEventListener('beforeunload', () => {
clearInterval(intervalId);
});
// Leak: Detached DOM nodes
let cache = [];
function addElement() {
const div = document.createElement('div');
cache.push(div); // Keeps reference even after removal
document.body.appendChild(div);
}
// Fixed: Clear references
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
// Don't store references to DOM nodes
}
Performance Monitoring
const startTime = performance.now();
heavyFunction();
const endTime = performance.now();
console.log(`Execution took ${endTime - startTime}ms`);
// Monitor long tasks (over 50ms)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime
});
}
});
observer.observe({ entryTypes: ['longtask'] });
// User Timing API
performance.mark('feature-start');
await loadFeature();
performance.mark('feature-end');
performance.measure('feature', 'feature-start', 'feature-end');
const measure = performance.getEntriesByName('feature')[0];
console.log(`Feature load: ${measure.duration}ms`);