Frontend Performance Optimization Techniques and Tools
Introduction
Performance optimization encompasses various aspects of web development that work together to create faster, more responsive applications. This article explores the key techniques and tools for optimizing frontend performence.
Performance optimization can be categorized into several areas:
- Metrics and Measurement Tools - Industry standards, optimization models, measurement tools, and performance APIs
- Code Optimization - JavaScript, HTML, and CSS performance improvements
- Rendering Optimization - Understanding browser rendering principles and optimizing rendering processes
- Resource Optimization - Compression, image formats, loading techniques, and font optimization
- Build Optimization - Webpack configuration, code splitting, compression, caching, and lazy loading
- Transfer and Loading Optimization - GZip, KeepAlive, HTTP caching, Service Workers, HTTP/2, SSR, and Nginx optimization
- Advanced Optimization Techniques - SVG icons, FlexBox layouts, preloading, pre-rendering, virtualized lists, and skeleton components
Performance Metrics and Tools
Network Tab (F12 Network)
The Network tab in browser developer tools provides detailed information about network requests:
- Network Settings (top-right):
- Use large request rows: Shows transferred network size and actual resource size
- Group by frame: Groups requests by page frames
- Show overview: Displays request timeline overview
- Capture screenshots: Records screenshots during loading
- Status Bar (bottom):
- Requests: Total number of requests
- Resources: Total resources loaded
- DOMContentLoaded: Time when DOM parsing completes (blue line in waterfall)
- Load: Total time for all resources to load (red line in waterfall)
- Waterfall Chart (above status bar):
- Queued at: Time request was queued
- Started at: Time request started
- Resource Scheduling:
- Queueing: Time spent in queue
- Connnection Start:
- Stalled: Time waiting before request starts
- DNS Lookup: Domain resolution time
- Initial connection: TCP handshake time
- SSL: SSL negotiation time
- Request/Response:
- Request sent: Time request was sent
- Waiting (TTFB): Time to first byte
- Content Download: Time to download content
- Explanation: Time spent parsing content
- Waterfall Menu (right-click):
- Save all as HAR with content: Save performance test results
- Filter Input: Filter requests by name
- Disable cache: Option to disable browser caching
- Throttling: Simulate different network conditions
Lighthouse (F12 Lighthouse)
Lighthouse provides comprehensive performance audits:
- Performance Score: Overall performance rating
- Metrics:
- First Contentful Paint: Time when first content is rendered
- Speed Index: Visual loading speed (target: 4.0s)
- Largest Contentful Paint: Time when largest content element is rendered
- Time to Interactive: Time until page is fully interactive
- Other Audits:
- Accessibility: Web accessibility standards
- Best Practices: Industry best practices
- SEO: Search engine optimization
- Progressive Web App: PWA capabilities
FPS Meter (F12 Frames per Second)
To display the frames per second meter:
- Press Ctrl+Shift+P
- Type "frame"
- Select "Show frames per second (FPS) meter"
RAIL Performance Model
RAIL is a user-centric performance model with four key metrics:
- Response: Interactive operations should complete within 50ms
- Animation: Produce a new frame every 10ms (60fps)
- Idle: Maximize idle time to perform background tasks
- Load: Content should load and become interactive within 5 seconds
Performance measurement tools:
- Chrome DevTools for development and debugging
- Lighthouse for comprehensive website quality assessment
- WebPageTest for multi-location testing and detailed reports
WebPageTest for Performance Evaluation
WebPageTest (webpagetest.org) provides detailed performance analysis:
- Test Configuration:
- Website URL: URL to test
- Test Location: Geographic location for testing
- Browser: Browser to use for testing
- Test Settings:
- Connection: Network connection type
- Number of Tests to Run: Number of test iterations
- Repeat View: Test both first visit and repeat visit with cache
- Capture Video: Record loading video
- Result Analysis:
- Performance Results:
- First Byte: Time to first response byte
- Start Render: Time when content first appears
- Total Blocking Time: Cumulative time main thread was blocked
- Test Results (Waterfall View):
- Page is Interactive: Time until page becomes interactive
- Browser Main Thread: Main thread usage analysis
- Performance Results:
- Local Deployment (Windows): ```
docker pull webpagetest/server
docker pull webpagetest/agent
docker run -d -p 4000:80 webpagetest/server
docker run -d -p 4001:80 --network="host" -e "SERVER_URL=http://localhost:4000/work/" -e "LOCATION=Test" webpagetest/agent
Access at localhost:4000
Lighthouse for Performance Analysis
Using Lighthouse programmatically:
# Install Lighthouse
npm i -g lighthouse
# Run Lighthouse test
lighthouse https://example.com/
# View results
# Open LH:Printer in browser
Understanding Lighthouse reports:
- Opportunities: Areas for improvement
- Remove unused JavaScript: Eliminate unused JS code
- Eliminate render-blocking resources: Remove resources that block rendering
- Diagnostics: Performance diagnostics
- Passed Audits: Performance best practices that are already implemented
Testing critical resource loading:
- Open DevTools (F12)
- Press Ctrl+Shift+P
- Type "show request blocking"
- Enable network request blocking
- Add patterns for resources to block (e.g., "log*.js")
- Analyze impact on page loading
Performance Tab (F12 Performance)
The Performance tab records and analyzes runtime performance:
- Recording:
- Record: Click to start recording, stop to finish
- Start profiling and reload page: Automatically records page load
- Metrics:
- Main (Main Thread): Function call stack analysis
- Timings: Key timing metrics like DOM load time
- Frames: Rendering frames and refresh rate
- Activity Analysis (Bottom):
- Summary: Overview of main activities
- Bottom-Up: Detailed breakdown of operations
Other Hidden Panels
Additional useful panels in DevTools:
- Network Request Blocking: Block specific network requests
- Rendering:
- Paint Flashing: Visualize repaints
- Layout Shift Regions: Identify layout shifts
- FPS Meter: Frames per second display
- Performance Monitor: Real-time performance metrics
Common Performance Measurement APIs
Web APIs for performance measurement:
- Standard Web APIs:
- Navigation Timing: Key navigation timing metrics
- Resource Timing: Resource loading timing
- Network APIs: Network status information
- HTTP Client Hints: Client-server negotiation
- UI APIs: Page visibility and state
Calculating key performance metrics:
// DNS resolution time
const dnsTime = performance.getEntriesByType('navigation')[0].domainLookupEnd
- performance.getEntriesByType('navigation')[0].domainLookupStart;
// TCP connection time
const tcpTime = performance.getEntriesByType('navigation')[0].connectEnd
- performance.getEntriesByType('navigation')[0].connectStart;
// SSL handshake time
const sslTime = performance.getEntriesByType('navigation')[0].connectEnd
- performance.getEntriesByType('navigation')[0].secureConnectionStart;
// Time to first byte (TTFB)
const ttfb = performance.getEntriesByType('navigation')[0].responseStart
- performance.getEntriesByType('navigation')[0].requestStart;
// Data transfer time
const transferTime = performance.getEntriesByType('navigation')[0].responseEnd
- performance.getEntriesByType('navigation')[0].responseStart;
// DOM parsing time
const domParseTime = performance.getEntriesByType('navigation')[0].domInteractive
- performance.getEntriesByType('navigation')[0].responseEnd;
// Resource loading time
const resourceLoadTime = performance.getEntriesByType('navigation')[0].loadEventStart
- performance.getEntriesByType('navigation')[0].domContentLoadedEventEnd;
// First Byte time
const firstByteTime = performance.getEntriesByType('navigation')[0].responseStart
- performance.getEntriesByType('navigation')[0].domainLookupStart;
// White screen time
const whiteScreenTime = performance.getEntriesByType('navigation')[0].responseEnd
- performance.getEntriesByType('navigation')[0].fetchStart;
// First Interactive Time
const firstInteractiveTime = performance.getEntriesByType('navigation')[0].domInteractive
- performance.getEntriesByType('navigation')[0].fetchStart;
// DOM Ready Time
const domReadyTime = performance.getEntriesByType('navigation')[0].domContentLoadEventEnd
- performance.getEntriesByType('navigation')[0].fetchStart;
// Page fully loaded time
const fullyLoadedTime = performance.getEntriesByType('navigation')[0].loadEventStart
- performance.getEntriesByType('navigation')[0].fetchStart;
// HTTP header size
const httpHeaderSize = performance.getEntriesByType('navigation')[0].transferSize
- performance.getEntriesByType('navigation')[0].encodedBodySize;
// Redirect count
const redirectCount = performance.getEntriesByType('navigation')[0].redirectCount;
// Redirect time
const redirectTime = performance.getEntriesByType('navigation')[0].redirectEnd
- performance.getEntriesByType('navigation')[0].redirectStart;
Example: Time to Interactive calculation
// Triggered after load event completes
window.addEventListener('load', function() {
// Calculate Time to Interactive
const timing = performance.getEntriesByType('navigation')[0];
const tti = timing.domInteractive - timing.fetchStart;
console.log("TTI: " + tti + "ms");
});
Example: Long tasks monitoring
// Monitor long tasks using PerformanceObserver
const taskObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.log('Long task detected:', entry.duration, 'ms');
}
});
// Start observing long tasks
taskObserver.observe({entryTypes: ['longtask']});
Example: Page visibility change detection
// Detect page visibility changes
let visibilityEvent = 'visibilitychange';
if (document.webkitHidden !== undefined) {
// WebKit-specific event name
visibilityEvent = "webkitvisibilitychange";
}
function handleVisibilityChange() {
if (document.hidden || document.webkitHidden) {
console.log("Page is not visible");
} else {
console.log("Page is visible");
}
}
document.addEventListener(visibilityEvent, handleVisibilityChange, false);
Example: Network connection monitoring
// Monitor network connection changes
const connection = navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;
const connectionType = connection.effectiveType;
function updateConnectionStatus() {
console.log("Connection type changed from " + connectionType +
" to " + connection.effectiveType);
}
connection.addEventListener('change', updateConnectionStatus);
Rendering Optimization
Browser Rendering Process and Critical Rendering Path
The browser's rendering process follows these steps:
- JavaScript: Script execution
- Style: CSS processing
- Layout: Calculating element positions and dimensions
- Paint: Drawing pixels to the screen
- Composite: Combining layers to form the final image
Browser object model construction:
- DOM Construction: HTML → DOM tree
- CSSOM Construction: CSS → CSSOM tree
Render tree construction:
- Render Tree: DOM + CSSOM → Render Tree
Layout and Painting
Layout and painting are critical performance aspects:
- Render Tree: Contains only nodes needed for display
- Layout: Calculates precise position and size of each node ("box model")
- Painting: Converts each node to pixels
Operations that trigger layout (reflow):
- Adding/removing elements
- Modifying styles
- Setting display: none
- Reading layout properties (offsetLeft, scrollTop, clientWidth)
- Moving elements
- Resizing browser window or changing font size
Avoiding layout thrashing:
- Minimize reflows
- Separate read and write operations
Using FastDOM
FastDOM helps batch DOM read and write operations to prevent layout thrashing:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Performance Optimization Example</title>
<script src="fastdom.min.js"></script>
<style>
.app {
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div class="app"></div>
<script>
// Using FastDOM to batch DOM read/write operations
window.addEventListener('load', function() {
const appElement = document.querySelector(".app");
let step = 2;
function animate() {
fastdom.measure(() => {
// Read operation - get current width
const currentWidth = appElement.offsetWidth;
fastdom.mutate(() => {
// Write operation - update width
if (currentWidth < 100 || currentWidth > 600) {
step *= -1;
}
appElement.style.width = `${currentWidth + step}px`;
});
});
window.requestAnimationFrame(animate);
}
animate();
});
</script>
</body>
</html>
Compositor Thread and Layers
The compositor thread manages layer composition:
- Compositing Process: Splits page into layers, draws them, then composites them
- Inspecting Layers:
- Press Ctrl+Shift+P
- Type "show layers"
- Compositor-Only Properties:
- transform: CSS transforms
- opacity: Opacity changes
Reducing Repaints
Strategies to minimize repaints:
- Identify Paint Bottlenecks:
- Open DevTools
- Press Ctrl+Shift+P
- Type "show rendering"
- Enable "Paint flashing" to visualize repaints
- Use will-change: Create new layers for elements that will change
Debouncing High-Frequency Event Handlers
Debouncing prevents excessive function calls during frequent events:
function processPointerMovement() {
console.log(new Date().getTime());
}
let isProcessing = false;
window.addEventListener('pointermove', event => {
if (isProcessing) return;
isProcessing = true;
window.requestAnimationFrame(() => {
processPointerMovement();
isProcessing = false;
});
});
React Time Scheduling Implementation
Understanding React's time scheduling:
- RequestIdleCallback Issues:
- Inconsistent availability across browsers
- May not fire when expected
- Simulating RequestIdleCallback with requestAnimationFrame:
- requestAnimationFrame executes before paint
- requestIdleCallback executes after paint during idle time
- Timeout parameter ensures tasks execute even if no idle time is available
Code Optimization
JavaScript Parsing Time Reduction
Understanding JavaScript performance bottlenecks:
- Performance Overheads:
- Loading
- Parsing & Compilation
- Execution
- Optimization Strategies:
- Code Splitting: Load code only when needed
- Tree Shaking: Remove unused code
- Main Thread Reduction:
- Avoid long-running tasks
- Avoid inline scripts larger than 1KB
- Use requestAnimationFrame and requestIdleCallback for scheduling
- Progressive Bootstrapping:
- Visible but non-interactive vs. minimally interactive resources
V8 Compilasion Principles
Understanding V8's compilation process:
// Testing optimization and deoptimization
// Use node --trace-opt --trace-deopt script.js to see optimization details
const {performance, PerformanceObserver} = require("perf_hooks");
const addNumbers = (a, b) => a + b;
const num1 = 1;
const num2 = 2;
performance.mark("start");
for (let i = 0; i < 10000000; i++) {
addNumbers(num1, num2);
}
// Intentionally cause deoptimization
addNumbers(num1, 'string');
// Continue execution
for (let i = 0; i < 10000000; i++) {
addNumbers(num1, num2);
}
performance.mark("end");
const observer = new PerformanceObserver(list => {
console.log(list.getEntries()[0]);
});
observer.observe({entryTypes: ['measure']});
performance.measure('Optimization Test', 'start', 'end');
V8 compilation process:
- Source Code → Abstract Syntax Tree (AST)
- AST → Bytecode
- Bytecode → Machine Code
V8 optimization mechanisms:
- Script Streaming: For scripts larger than 30KB, parsing begins while downloading
- Bytecode Caching: Caches compiled bytecode for reuse
- Lazy Parsing: Functions aren't parsed until they're executed
Function Optimization
Optimizing JavaScript functions:
- Parsing Methods:
- Lazy Parsing: Default method - functions parsed when executed
- Eager Parsing: Functions parsed immediately when declared (add parentheses to function body)
- Optimize.js: Tool to optimize initial load time
Object Optimization
Optimizing JavaScript objects:
- Hidden Classes:
- Initialize object properties in the same order to avoid hidden class transitions
- Don't add new properties after object instantiation
- Array Optimization:
- Use arrays instead of array-like objects
- Avoid reading beyond array length
- Avoid type conversions in arrays
Hidden classes example:
// Object with consistent property order
class Rectangle {
constructor(length, width) {
this.length = length; // Hidden class transition 1
this.width = width; // Hidden class transition 2
}
}
const rect1 = new Rectangle(3, 4);
const rect2 = new Rectangle(5, 6);
// Object with inconsistent property order
const car1 = {color: "red"}; // Hidden class 0
car1.seats = 4; // Hidden class 1
const car2 = {seats: 2}; // Hidden class 2
car2.color = "blue"; // Hidden class 3
In-object properties vs. normal properties:
// In-object property (stored directly in object)
const car = {color: "red"};
// Normal property (stored in property store)
car.seats = 4; // Requires indirection through property array
Array-like objects vs. true arrays:
// Less efficient - array-like object
Array.prototype.forEach.call(arrayLike, (value, index) => {
console.log(`${index}:${value}`);
});
// More efficient - convert to real array
const realArray = Array.prototype.slice.call(arrayLike, 0);
realArray.forEach((value, index) => {
console.log(`${index}:${value}`);
});
Array bounds checking:
1000) {
console.log(array[i]); // May produce invalid results
}
}
}
Array type transitions:
HTML Optimization
Optimizing HTML structure:
- Reduce iframes: Minimize iframe usage where possible
- Compress whitespace: Remove unnecessary whitespace
- Avoid deep nesting: Limit DOM depth
- Avoid table layouts: Use modern CSS layout techniques
- Remove comments: Clean up HTML comments
- Externalize CSS and JavaScript: Link to external files
- Remove default attributes: Don't specify default values
Tools for HTML optimization:
- html-minifier: Tool to minify HTML
Lazy loading iframes:
<iframe id="content-frame"></iframe>
// Load iframe content when needed
document.getElementById("content-frame").setAttribute('src', 'content-url');
Optimized HTML structure:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Optimized Page</title>
<!-- External CSS -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Minimal, semantic structure -->
<header></header>
<main>
<section></section>
<section></section>
</main>
<footer></footer>
<!-- External JavaScript at bottom -->
<script src="scripts.js"></script>
</body>
</html>
CSS Performance Impact
Understanding CSS performance implications:
- Style Calculation Overhead:
- Measure style calculation in DevTools:
- Open DevTools
- Go to Performance tab
- Look at "Recalculate Style" in the main thread
- Measure style calculation in DevTools:
- CSS Selector Matching:
- Selectors are matched right-to-left
- Optimize selectors to be efficient
- CSS Optimization Strategies:
- Reduce CSS Rendering Blocking: Use async loading for non-critical CSS
- Use GPU for Animations: Utilize transform and opacity for animations
- Contain Property:
contain: layout;: Prevents internal elements from affecting external layout
- Font Display Property: Control font loading behavior with
font-display