Introduction to JavaScript Performance
In our previous lectures, we explored performance testing tools and load testing with Artillery. Now we'll shift our focus to optimizing the performance of JavaScript applications. As applications grow more complex, performance becomes a critical factor in user experience and business success.
Performance optimization isn't just about making your code run faster—it's about creating better experiences for your users. Slow applications frustrate users, increase bounce rates, and can negatively impact business metrics. According to research, users typically abandon sites that take more than 3 seconds to load, making performance optimization not just a technical concern but a business imperative.
The Performance Optimization Process
Why Performance Matters
Before diving into specific optimization techniques, let's understand why JavaScript performance matters for modern web applications:
- User Experience: Fast applications create better user experiences, leading to higher engagement and conversion rates
- Mobile Considerations: Mobile devices often have less processing power and may be on slower networks, making performance even more critical
- SEO Impact: Search engines like Google consider page speed in their ranking algorithms
- Business Metrics: Faster sites have lower bounce rates and higher conversion rates
- Global Reach: Users across the globe with varying network conditions and device capabilities can access your application
Real-World Performance Impact
- Pinterest reduced perceived wait times by 40% and increased search engine traffic and sign-ups by 15%
- The BBC found they lost 10% of users for every additional second their site took to load
- Walmart saw a 2% conversion increase for every 1 second of improvement in page load time
Measuring Performance
Before optimizing, you need to know what to optimize. Effective performance optimization starts with accurate measurement.
Browser Development Tools
- Performance Panel: Analyze rendering, JavaScript execution, and memory usage
- Network Panel: Analyze request timing, size, and waterfall
- Memory Panel: Identify memory leaks and analyze heap snapshots
- Lighthouse: Get comprehensive performance audits and recommendations
Web Vitals and Core Metrics
Modern performance optimization focuses on user-centric metrics that measure real user experience:
| Metric | Description | Target |
|---|---|---|
| Largest Contentful Paint (LCP) | Time to render the largest content element | < 2.5s |
| First Input Delay (FID) | Time from first user interaction to response | < 100ms |
| Cumulative Layout Shift (CLS) | Unexpected layout shifts during page load | < 0.1 |
| First Contentful Paint (FCP) | Time to render first DOM content | < 1.8s |
| Time to Interactive (TTI) | Time until page is fully interactive | < 3.8s |
| Total Blocking Time (TBT) | Sum of time main thread is blocked | < 200ms |
JavaScript Performance APIs
JavaScript provides built-in APIs for measuring performance within your application:
// Mark the beginning of a performance measurement
performance.mark('myFunction-start');
// Run function to measure
myFunction();
// Mark the end of the performance measurement
performance.mark('myFunction-end');
// Create a performance measure
performance.measure(
'myFunction',
'myFunction-start',
'myFunction-end'
);
// Get all measurements
const measurements = performance.getEntriesByType('measure');
console.log(measurements);
// Clear marks and measures
performance.clearMarks();
performance.clearMeasures();
User-centric Measurement
Don't forget to collect real-world performance data from actual users:
- Real User Monitoring (RUM): Tools like Google Analytics, New Relic, or custom solutions using the Performance API
- Field Data vs Lab Data: Balance controlled testing (lab) with real-world usage (field)
- Performance Budget: Set thresholds for key metrics and monitor regularly
Optimizing Load Time
The first impression users have of your application is how quickly it loads. Here are key techniques to optimize load time:
JavaScript Loading Strategies
Async and Defer Attributes
<!-- Regular script: blocks parsing -->
<script src="regular.js"></script>
<!-- Async: downloads in parallel, executes as soon as available -->
<script src="analytics.js" async></script>
<!-- Defer: downloads in parallel, executes after parsing -->
<script src="app.js" defer></script>
Script Loading Comparison
Code Splitting
Modern bundlers like Webpack, Rollup, or Vite allow you to split your JavaScript into smaller chunks that can be loaded on demand:
Dynamic Import Example
// Instead of importing everything upfront
// import { heavyFunction } from './heavy-module';
// Use dynamic import when needed
button.addEventListener('click', async () => {
// This code only loads when the button is clicked
const { heavyFunction } = await import('./heavy-module');
heavyFunction();
});
Tree Shaking
Tree shaking removes unused code from your bundles. Modern bundlers support this automatically for ES modules:
Before Tree Shaking
// utils.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// app.js
import { add } from './utils';
console.log(add(2, 3)); // Only using add
After tree shaking, the subtract function is removed from the final bundle.
Compression and Minification
Reduce file size with these essential techniques:
- Minification: Remove whitespace, comments, and shorten variable names
- Compression: Use Gzip or Brotli to compress files during transfer
- Source Maps: Generate source maps for debugging minified code
Example Webpack Configuration for Minification
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
// ... other configuration
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
},
output: {
comments: false, // Remove comments
},
},
}),
],
},
};
Caching Strategies
Properly configured caching can dramatically improve load times for returning visitors:
- HTTP Caching: Set appropriate Cache-Control headers
- Content Hashing: Include content hash in filenames to enable long-term caching
- Service Workers: Cache assets for offline use and faster repeat visits
Content Hashing in Webpack
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
};
Preloading and Prefetching
Give browsers hints about resources they'll need soon:
<!-- Preload critical resources -->
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="critical.css" as="style">
<!-- Prefetch resources that might be needed later -->
<link rel="prefetch" href="non-critical.js">
Optimizing Runtime Performance
Once your application has loaded, runtime performance becomes crucial for smooth interactions. Here are key techniques to optimize runtime performance:
Efficient DOM Manipulation
The DOM is often the bottleneck in web applications. Minimize DOM operations with these techniques:
Use DocumentFragment for Batch Updates
// Inefficient: Multiple DOM updates
for (let i = 0; i < 1000; i++) {
const listItem = document.createElement('li');
listItem.textContent = `Item ${i}`;
document.getElementById('myList').appendChild(listItem);
}
// Efficient: Batch DOM updates with DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const listItem = document.createElement('li');
listItem.textContent = `Item ${i}`;
fragment.appendChild(listItem);
}
document.getElementById('myList').appendChild(fragment);
Minimize Reflows and Repaints
// Inefficient: Causes multiple reflows
const element = document.getElementById('myElement');
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';
// Efficient: Batches style changes
const element = document.getElementById('myElement');
element.style.cssText = 'width: 100px; height: 100px; background-color: red;';
// Or use classes
element.classList.add('styled-element');
Event Handling Optimization
Poorly implemented event handling can significantly impact performance:
Event Delegation
// Inefficient: Attaching many event listeners
document.querySelectorAll('.button').forEach(button => {
button.addEventListener('click', handleClick);
});
// Efficient: Using event delegation
document.querySelector('.button-container').addEventListener('click', (event) => {
if (event.target.matches('.button')) {
handleClick(event);
}
});
Debouncing and Throttling
// Debounce: Only execute after a period of inactivity
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Throttle: Execute at most once per specified period
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage
const debouncedSearch = debounce(searchFunction, 300);
const throttledScroll = throttle(scrollHandler, 100);
window.addEventListener('scroll', throttledScroll);
searchInput.addEventListener('input', debouncedSearch);
Memory Management
JavaScript's garbage collection is automatic, but poor coding patterns can still cause memory leaks:
Common Memory Leak Patterns
// Leak 1: Forgetting to remove event listeners
function setupEvents() {
const button = document.getElementById('myButton');
button.addEventListener('click', handleClick);
}
// Fix: Remove event listeners when no longer needed
function cleanup() {
const button = document.getElementById('myButton');
button.removeEventListener('click', handleClick);
}
// Leak 2: Circular references
function createClosure() {
const data = { counter: 0 };
// Element references closure, closure references element
const element = document.getElementById('counter');
element.data = data;
data.increment = function() {
this.counter++;
element.textContent = this.counter;
};
return data;
}
// Fix: Break circular references
function createClosure() {
const data = { counter: 0 };
const element = document.getElementById('counter');
data.increment = function() {
this.counter++;
// Use a selector instead of storing the element
document.getElementById('counter').textContent = this.counter;
};
return data;
}
Monitoring Memory Usage
// Check memory usage periodically
function logMemoryUsage() {
if (performance.memory) {
console.log('Memory usage:', {
total: Math.round(performance.memory.totalJSHeapSize / (1024 * 1024)) + ' MB',
used: Math.round(performance.memory.usedJSHeapSize / (1024 * 1024)) + ' MB',
limit: Math.round(performance.memory.jsHeapSizeLimit / (1024 * 1024)) + ' MB'
});
}
}
// Call every 10 seconds
setInterval(logMemoryUsage, 10000);
Efficient Data Structures and Algorithms
Choosing the right data structures can significantly impact performance:
Map vs Object for Lookups
// Testing lookup performance with different data structures
function testLookupPerformance() {
const iterations = 1000000;
const keys = [];
// Generate random keys
for (let i = 0; i < 1000; i++) {
keys.push(`key${i}`);
}
// Setup Object
const obj = {};
for (const key of keys) {
obj[key] = key;
}
// Setup Map
const map = new Map();
for (const key of keys) {
map.set(key, key);
}
// Test Object lookup
console.time('Object lookup');
for (let i = 0; i < iterations; i++) {
const randomKey = keys[i % keys.length];
const value = obj[randomKey];
}
console.timeEnd('Object lookup');
// Test Map lookup
console.time('Map lookup');
for (let i = 0; i < iterations; i++) {
const randomKey = keys[i % keys.length];
const value = map.get(randomKey);
}
console.timeEnd('Map lookup');
}
testLookupPerformance();
Set for Unique Values
// Inefficient: Using array to check for duplicates
function uniqueArray(arr) {
const result = [];
for (const item of arr) {
if (!result.includes(item)) { // O(n) operation
result.push(item);
}
}
return result;
}
// Efficient: Using Set
function uniqueArray(arr) {
return [...new Set(arr)]; // O(1) lookup time
}
// Compare performance
const largeArray = Array.from({ length: 10000 }, (_, i) => i % 1000);
console.time('Array method');
uniqueArray(largeArray);
console.timeEnd('Array method');
console.time('Set method');
uniqueArray(largeArray);
console.timeEnd('Set method');
Worker Threads for CPU-Intensive Tasks
JavaScript is single-threaded, but Web Workers enable parallel processing:
Main Thread (main.js)
// Create a worker
const worker = new Worker('worker.js');
// Send data to the worker
worker.postMessage({
task: 'processData',
data: largeDataArray
});
// Receive results from the worker
worker.onmessage = function(event) {
const result = event.data;
console.log('Processing complete:', result);
};
// UI remains responsive while the worker processes data
document.querySelector('#status').textContent = 'Processing in background...';
Worker Thread (worker.js)
// Listen for messages from the main thread
self.onmessage = function(event) {
const { task, data } = event.data;
if (task === 'processData') {
// CPU-intensive processing
const result = processLargeData(data);
// Send result back to main thread
self.postMessage(result);
}
};
function processLargeData(data) {
// Simulate CPU-intensive work
let result = 0;
for (let i = 0; i < data.length; i++) {
// Complex calculations...
result += Math.sqrt(data[i] * data[i]);
}
return result;
}
Framework-Specific Optimizations
Modern JavaScript frameworks have their own performance considerations. Here are some tips for common frameworks:
React Optimizations
React.memo for Component Memoization
// Wrap functional components with React.memo to prevent unnecessary re-renders
const MyComponent = React.memo(function MyComponent(props) {
return (
{props.title}
{props.content}
);
});
// Custom comparison function for complex props
const MyComplexComponent = React.memo(
function MyComplexComponent(props) {
return {/* component logic */};
},
(prevProps, nextProps) => {
// Return true if passing nextProps to render would return
// the same result as passing prevProps
return prevProps.data.id === nextProps.data.id;
}
);
useMemo and useCallback Hooks
function SearchResults({ query, data }) {
// Memoize expensive calculations
const filteredData = useMemo(() => {
console.log('Filtering data...');
return data.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
);
}, [data, query]); // Only recalculate when data or query changes
// Memoize callback functions
const handleItemClick = useCallback((item) => {
console.log('Item clicked:', item);
// Handle the click event
}, []); // No dependencies, function never changes
return (
<ul>
{filteredData.map(item => (
<li key={item.id} onClick={() => handleItemClick(item)}>
{item.title}
</li>
))}
</ul>
);
}
Virtual List for Large Datasets
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
// Render only the visible items in a large list
const Row = ({ index, style }) => (
<div style={style}>
Item {index}: {items[index]}
</div>
);
return (
<FixedSizeList
height={400}
width={300}
itemCount={items.length}
itemSize={35}
>
{Row}
</FixedSizeList>
);
}
Vue Optimizations
Use v-show for Conditional Display
<!-- v-if removes/adds elements from DOM (higher cost) -->
<div v-if="isVisible">Heavy content</div>
<!-- v-show just toggles display: none (lower cost if toggled frequently) -->
<div v-show="isVisible">Heavy content</div>
Use Computed Properties
export default {
data() {
return {
items: [...], // Large array
query: ''
};
},
computed: {
// Automatically cached until dependencies change
filteredItems() {
return this.items.filter(item =>
item.name.toLowerCase().includes(this.query.toLowerCase())
);
}
}
}
Functional Components for Simple Components
<!-- Simple components can be defined as functional components -->
<template functional>
<div class="item">
<h3>{{ props.title }}</h3>
<p>{{ props.description }}</p>
</div>
</template>
Angular Optimizations
OnPush Change Detection
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-item',
template: `
<div class="item">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ItemComponent {
@Input() item: any;
}
TrackBy Function with ngFor
@Component({
selector: 'app-list',
template: `
<!-- Use trackBy to avoid unnecessary DOM recreation -->
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
`
})
export class ListComponent {
items = [...]; // List of items
trackByFn(index, item) {
return item.id; // Use unique identifier
}
}
Lazy Loading Modules
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'reports',
loadChildren: () => import('./reports/reports.module').then(m => m.ReportsModule)
}
];
Leveraging Modern Browser Features
Modern browsers offer powerful features that can significantly improve performance:
Intersection Observer for Lazy Loading
// Lazy load images when they enter the viewport
const images = document.querySelectorAll('.lazy-image');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.getAttribute('data-src');
if (src) {
img.src = src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
}
});
});
images.forEach(img => {
imageObserver.observe(img);
});
Resize Observer for Responsive Optimizations
// Optimize rendering based on element size
const chartContainer = document.querySelector('#chart');
const chart = createChart(chartContainer);
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
// Instead of listening to window resize events,
// we get notified when this specific element resizes
const { width, height } = entry.contentRect;
// Optimize rendering based on available space
if (width < 600) {
chart.setOptions({ showLegend: false, simplified: true });
} else {
chart.setOptions({ showLegend: true, simplified: false });
}
chart.resize(width, height);
}
});
resizeObserver.observe(chartContainer);
WebGL for Complex Visualizations
// Instead of manipulating thousands of DOM elements for visualization
// Use WebGL or Canvas for high-performance rendering
import * as THREE from 'three';
function createDataVisualization(container, data) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
// Add thousands of points without impacting DOM performance
const geometry = new THREE.BufferGeometry();
const vertices = [];
// Convert data to 3D points
data.forEach(item => {
vertices.push(item.x, item.y, item.z);
});
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
const material = new THREE.PointsMaterial({ color: 0x888888, size: 0.1 });
const points = new THREE.Points(geometry, material);
scene.add(points);
camera.position.z = 5;
function animate() {
requestAnimationFrame(animate);
points.rotation.x += 0.001;
points.rotation.y += 0.002;
renderer.render(scene, camera);
}
animate();
}
Deployment and Delivery Optimizations
Optimizing how your code is delivered to users can have a significant impact on performance:
CDN Deployment
Content Delivery Networks distribute your assets to servers worldwide, reducing latency for users:
- Deploy static assets (JS, CSS, images) to CDNs
- Use multiple CDN domains for parallel downloads
- Consider edge computing for dynamic content
Modern Protocol Support
Configure your server to use the latest web protocols:
- HTTP/2: Multiplexing, header compression, server push
- HTTP/3: Improved performance over unreliable connections
- Brotli Compression: More efficient than gzip
Nginx Configuration for HTTP/2 and Brotli
server {
listen 443 ssl http2;
server_name example.com;
# SSL configuration
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Enable Brotli compression
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
# Static assets with cache headers
location /assets/ {
root /var/www/example.com;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# API endpoints
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Differential Serving
Serve optimized code based on browser capabilities:
Module/Nomodule Pattern
<!-- Modern browsers get smaller, optimized ES modules bundle -->
<script type="module" src="app.modern.js"></script>
<!-- Legacy browsers get transpiled code with polyfills -->
<script nomodule src="app.legacy.js"></script>
Babel Configuration for Differential Builds
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
// Modern build - target browsers with ES modules support
...(process.env.BROWSERSLIST_ENV === 'modern' ? {
targets: { esmodules: true },
bugfixes: true,
loose: true
} :
// Legacy build - wider browser support
{
targets: '> 0.25%, not dead',
useBuiltIns: 'usage',
corejs: 3
})
}]
]
};
Implementing a Performance Optimization Workflow
Performance optimization should be an ongoing process integrated into your development workflow:
Performance Budgets
Set measurable targets for performance metrics:
Webpack Performance Budget
// webpack.config.js
module.exports = {
// ...
performance: {
hints: 'error',
maxAssetSize: 100000, // 100 KB
maxEntrypointSize: 300000, // 300 KB
},
};
Lighthouse CI Configuration
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['https://example.com/'],
numberOfRuns: 3,
},
assert: {
assertions: {
'first-contentful-paint': ['warn', {maxNumericValue: 2000}],
'interactive': ['error', {maxNumericValue: 3000}],
'max-potential-fid': ['error', {maxNumericValue: 100}],
'cumulative-layout-shift': ['error', {maxNumericValue: 0.1}],
'largest-contentful-paint': ['error', {maxNumericValue: 2500}],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
Automated Performance Testing
Integrate performance testing into your CI/CD pipeline:
- Run Lighthouse for each pull request
- Track Core Web Vitals over time
- Compare performance before and after changes
- Block merges that degrade performance beyond thresholds
Performance Monitoring in Production
Monitor real-world performance to identify issues:
- Implement Real User Monitoring (RUM)
- Track Core Web Vitals in your analytics
- Set up alerts for performance regressions
- Analyze performance by device, region, and connection type
Capturing Performance Metrics with Web Vitals
import { getCLS, getFID, getLCP } from 'web-vitals';
function sendToAnalytics({ name, delta, id }) {
// Send metrics to your analytics platform
navigator.sendBeacon('/analytics', JSON.stringify({
name,
delta,
id,
// Add additional info
page: window.location.pathname,
timestamp: Date.now()
}));
}
// Register listeners to capture and report Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
Practical Activity: Performance Audit and Optimization
Let's put our knowledge into practice by optimizing a real application. In this exercise, we'll identify and fix performance issues in a sample application.
Step 1: Performance Audit
- Clone the sample application repository:
git clone https://github.com/example/performance-workshop.git - Install dependencies and start the development server:
npm install npm start - Open Chrome DevTools and run a Lighthouse audit
- Identify the top 3 performance issues
- Use the Performance panel to profile JavaScript execution
Step 2: Implement Optimizations
Based on your audit, implement at least three of the following optimizations:
- Code splitting for large components
- Implement lazy loading for images
- Optimize event handlers with debouncing or throttling
- Improve rendering performance by reducing DOM operations
- Add memoization for expensive calculations
- Fix memory leaks
Step 3: Measure Improvements
- Run another Lighthouse audit to compare before and after scores
- Quantify the improvements in each key metric
- Use the Performance API to measure specific optimizations
Step 4: Group Discussion
Share your findings with the class:
- What were the biggest performance bottlenecks?
- Which optimizations had the most impact?
- What challenges did you encounter during optimization?
- How much improvement did you achieve in key metrics?
Key Takeaways
Performance optimization is a continuous process, not a one-time task. Here are the key principles to remember:
- Measure First: Always start by measuring performance to identify real bottlenecks
- Focus on User Experience: Prioritize optimizations that improve perceived performance
- Progressive Enhancement: Build a core experience that works for everyone, then enhance for modern browsers
- Loading Optimization: Minimize, compress, and deliver assets efficiently
- Runtime Optimization: Optimize DOM operations, event handling, and memory usage
- Automate: Integrate performance testing into your development workflow
- Monitor: Track real-world performance to catch regressions
Further Resources
Homework Assignment
For your homework, you'll apply performance optimization techniques to your own project:
Assignment Requirements
- Choose a project to optimize:
- Your final project application
- A personal or work project
- The provided sample application with additional features
- Perform a baseline performance audit:
- Run a Lighthouse audit
- Capture key metrics (LCP, FID, CLS, etc.)
- Profile JavaScript execution
- Document baseline performance
- Implement at least five optimizations from different categories:
- Loading optimizations (code splitting, lazy loading, etc.)
- Runtime optimizations (DOM, events, memory, etc.)
- Framework-specific optimizations
- Build and delivery optimizations
- Measure the impact of your optimizations:
- Run post-optimization audits
- Quantify improvements in key metrics
- Document the most effective optimizations
- Create a performance optimization report:
- Baseline performance analysis
- Identified bottlenecks
- Implemented optimizations with code examples
- Performance improvements with metrics
- Recommendations for further improvements
Submission Guidelines
- Code repository with before/after branches
- Performance optimization report (2-3 pages)
- Lighthouse reports (before and after)
- Any custom performance measurements or profiles
Due Date
Submit your completed assignment before our next class session.