Security & Performance
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:
<!-- Before: Large single bundle -->
<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 -->
<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 -->
Note: Modern bundlers like Webpack, Rollup, and Vite can reduce bundle sizes by 40-70% through minification, compression, and tree shaking.
Tree Shaking
Tree shaking eliminates unused code from your bundles. It works with ES6 modules:
// Bad: Imports entire library
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
}
};
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
}
};
Tip: Use bundle analyzers like webpack-bundle-analyzer to visualize which dependencies are bloating your bundle.
Code Splitting
Split code into smaller chunks that load on demand:
// Route-based code splitting (React example)
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();
});
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:
// Intersection Observer for lazy loading
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);
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);
Warning: Don't lazy load critical above-the-fold content or features needed for initial interactivity. This can harm user experience and Core Web Vitals.
Web Workers
Offload heavy computations to background threads:
// worker.js
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();
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:
// Bad: Updates outside animation frame
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';
});
}
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:
// Leak: Event listeners not removed
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
}
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
}
Tip: Use Chrome DevTools Memory Profiler to detect memory leaks. Take heap snapshots before and after actions to identify objects that aren't being garbage collected.
Performance Monitoring
// Measure script execution time
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`);
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`);
Exercise: Analyze your application's JavaScript bundle using webpack-bundle-analyzer. Identify the three largest dependencies and research tree-shakeable alternatives. Implement code splitting for at least two routes and measure the impact on initial load time using Lighthouse.