13 KiB
13 KiB
Runtime Debugging Patterns
Advanced techniques for debugging web application freezes, hangs, and performance issues.
Common Runtime Issues
1. Memory Leaks
Symptoms:
- Gradually increasing memory usage
- Browser tab becomes unresponsive over time
- Page crashes with "Aw, Snap!" error
Common Causes:
// Detached DOM nodes
let elements = [];
document.addEventListener('click', () => {
const div = document.createElement('div');
document.body.appendChild(div);
elements.push(div); // Keeping reference after removal
document.body.removeChild(div); // DOM node removed but still in memory
});
// Event listener accumulation
class Component {
constructor() {
// Missing cleanup in componentWillUnmount/ngOnDestroy
window.addEventListener('resize', this.handleResize);
}
}
// Closure traps
function createLeak() {
const largeData = new Array(1000000).fill('data');
return function() {
// largeData is retained even if not used
console.log('callback');
};
}
Detection:
- Use Chrome DevTools Memory Profiler
- Take heap snapshots and compare
- Look for objects with increasing retain counts
2. Infinite Loops
JavaScript Infinite Loops:
// Synchronous infinite loop - blocks immediately
while (condition) {
// condition never becomes false
}
// Recursive setTimeout - gradual degradation
function loop() {
// Do something
setTimeout(loop, 0); // Keeps scheduling
}
Angular Change Detection Loops:
// Component causing infinite change detection
@Component({
template: '{{ getComputedValue() }}'
})
class BadComponent {
getComputedValue() {
// Returns different value each time
return Math.random(); // Triggers new change detection
}
}
// Zone.js microtask explosion
someObservable.subscribe(value => {
this.property = value;
// Triggers change detection
this.anotherObservable.next(this.property);
// Which triggers another change...
});
React Re-render Loops:
// Missing dependencies
function Component() {
const [state, setState] = useState(0);
useEffect(() => {
setState(state + 1); // Missing dependency array
}); // Runs on every render
return <div>{state}</div>;
}
// Object/Array recreation
function Parent() {
// New object every render
const config = { key: 'value' };
return <Child config={config} />;
// Child re-renders every time
}
3. Main Thread Blocking
Long-Running JavaScript:
// Heavy computation on main thread
function processLargeDataset(data) {
for (let i = 0; i < 1000000; i++) {
// Complex calculations
data[i] = expensiveOperation(data[i]);
}
}
// Solution: Use Web Workers
const worker = new Worker('processor.js');
worker.postMessage({ cmd: 'process', data: largeData });
worker.onmessage = (e) => {
// Handle processed data
};
// Or chunk the work
async function processInChunks(data) {
const chunkSize = 100;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
processChunk(chunk);
// Yield to browser
await new Promise(resolve => setTimeout(resolve, 0));
}
}
4. Network Request Issues
Request Queue Blocking:
// Browser limits concurrent requests (6 per domain)
for (let i = 0; i < 100; i++) {
fetch(`/api/item/${i}`); // Only 6 run in parallel
}
// Solution: Batch requests
const ids = Array.from({ length: 100 }, (_, i) => i);
fetch('/api/items/batch', {
method: 'POST',
body: JSON.stringify({ ids })
});
CORS Preflight Accumulation:
// Each cross-origin request triggers OPTIONS
for (let endpoint of endpoints) {
fetch(endpoint, {
headers: {
'Custom-Header': 'value' // Triggers preflight
}
});
}
Framework-Specific Patterns
Angular Debugging
Zone.js Task Tracking:
// Monitor Zone.js tasks
Zone.current.fork({
name: 'debugZone',
onScheduleTask: (delegate, current, target, task) => {
console.log('Task scheduled:', task.type, task.source);
return delegate.scheduleTask(target, task);
},
onInvokeTask: (delegate, current, target, task, applyThis, applyArgs) => {
console.log('Task invoked:', task.type);
return delegate.invokeTask(target, task, applyThis, applyArgs);
}
}).run(() => {
// Your Angular app code
});
Change Detection Profiling:
// Enable Angular DevTools Profiler
import { enableDebugTools } from '@angular/platform-browser';
import { ApplicationRef } from '@angular/core';
platformBrowserDynamic().bootstrapModule(AppModule)
.then(module => {
const appRef = module.injector.get(ApplicationRef);
const componentRef = appRef.components[0];
enableDebugTools(componentRef);
// Now use: ng.profiler.timeChangeDetection()
});
React Debugging
React Profiler API:
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
<Profiler id="App" onRender={onRenderCallback}>
<App />
</Profiler>
Why Did You Render:
// Setup why-did-you-render
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
// Track specific component
Component.whyDidYouRender = true;
Vue Debugging
Vue Performance Tracking:
// Vue 2
Vue.config.performance = true;
// Vue 3
const app = createApp(App);
app.config.performance = true;
// Now use Performance DevTools to see component timings
Browser APIs for Debugging
Performance Observer API
// Monitor long tasks
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name
});
// Send to analytics
analytics.track('long_task', {
duration: entry.duration,
url: window.location.href
});
}
}
});
observer.observe({
entryTypes: ['longtask', 'measure', 'navigation']
});
Memory API
// Monitor memory usage (Chrome only)
if (performance.memory) {
setInterval(() => {
const memInfo = performance.memory;
const usedMB = Math.round(memInfo.usedJSHeapSize / 1048576);
const totalMB = Math.round(memInfo.totalJSHeapSize / 1048576);
const limitMB = Math.round(memInfo.jsHeapSizeLimit / 1048576);
console.log(`Memory: ${usedMB}/${totalMB}MB (limit: ${limitMB}MB)`);
// Alert if using >90% of limit
if (memInfo.usedJSHeapSize > memInfo.jsHeapSizeLimit * 0.9) {
console.error('Memory usage critical!');
}
}, 5000);
}
Request Idle Callback
// Defer non-critical work
const tasksQueue = [];
function scheduleTasks(tasks) {
tasksQueue.push(...tasks);
processTaskQueue();
}
function processTaskQueue(deadline) {
while (deadline.timeRemaining() > 0 && tasksQueue.length > 0) {
const task = tasksQueue.shift();
task();
}
if (tasksQueue.length > 0) {
requestIdleCallback(processTaskQueue);
}
}
requestIdleCallback(processTaskQueue);
Headless Browser Debugging
Puppeteer Performance Monitoring
const puppeteer = require('puppeteer');
async function analyzePerformance(url) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Enable CDP domains
const client = await page.target().createCDPSession();
await client.send('Performance.enable');
await client.send('Network.enable');
// Collect metrics
const metrics = [];
// Monitor metrics over time
const interval = setInterval(async () => {
const { metrics: perfMetrics } = await client.send('Performance.getMetrics');
const timestamp = Date.now();
metrics.push({
timestamp,
heap: perfMetrics.find(m => m.name === 'JSHeapUsedSize').value,
documents: perfMetrics.find(m => m.name === 'Documents').value,
frames: perfMetrics.find(m => m.name === 'Frames').value,
nodes: perfMetrics.find(m => m.name === 'Nodes').value,
layoutCount: perfMetrics.find(m => m.name === 'LayoutCount').value,
jsEventListeners: perfMetrics.find(m => m.name === 'JSEventListeners').value
});
}, 1000);
// Navigate and wait
await page.goto(url, { waitUntil: 'networkidle0' });
await page.waitForTimeout(10000);
clearInterval(interval);
// Analyze trends
const analysis = analyzeMetricTrends(metrics);
await browser.close();
return analysis;
}
function analyzeMetricTrends(metrics) {
const heapGrowth = metrics[metrics.length - 1].heap - metrics[0].heap;
const avgHeapGrowthPerSec = heapGrowth / metrics.length;
return {
memoryLeak: avgHeapGrowthPerSec > 100000, // >100KB/s growth
domLeak: metrics[metrics.length - 1].nodes > 1500,
listenerLeak: metrics[metrics.length - 1].jsEventListeners > 500,
metrics: metrics
};
}
Playwright Network Monitoring
const { chromium } = require('playwright');
async function monitorNetworkPerformance(url) {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const slowRequests = [];
const failedRequests = [];
// Monitor all requests
page.on('request', request => {
request._startTime = Date.now();
});
page.on('response', response => {
const request = response.request();
const duration = Date.now() - request._startTime;
if (duration > 3000) {
slowRequests.push({
url: request.url(),
method: request.method(),
duration: duration,
status: response.status()
});
}
});
page.on('requestfailed', request => {
failedRequests.push({
url: request.url(),
method: request.method(),
failure: request.failure()
});
});
await page.goto(url);
await page.waitForTimeout(10000);
await browser.close();
return { slowRequests, failedRequests };
}
Production Monitoring
Client-Side Monitoring Script
// Add to your production app
(function() {
const perfData = {
longTasks: [],
errors: [],
metrics: []
};
// Long task monitoring
if (window.PerformanceObserver) {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
perfData.longTasks.push({
duration: entry.duration,
timestamp: Date.now()
});
// Send to backend if critical
if (entry.duration > 1000) {
sendToAnalytics('critical_long_task', {
duration: entry.duration,
url: window.location.href
});
}
}
}).observe({ entryTypes: ['longtask'] });
}
// Error monitoring
window.addEventListener('error', (e) => {
perfData.errors.push({
message: e.message,
stack: e.error?.stack,
timestamp: Date.now()
});
sendToAnalytics('js_error', {
message: e.message,
url: window.location.href
});
});
// Periodic metrics collection
setInterval(() => {
if (performance.memory) {
perfData.metrics.push({
heap: performance.memory.usedJSHeapSize,
timestamp: Date.now()
});
}
}, 10000);
// Send aggregated data on page unload
window.addEventListener('beforeunload', () => {
navigator.sendBeacon('/api/performance', JSON.stringify(perfData));
});
function sendToAnalytics(event, data) {
// Your analytics implementation
if (typeof gtag !== 'undefined') {
gtag('event', event, data);
}
}
})();
Quick Diagnosis Checklist
When app freezes/hangs:
- Check Console: Any errors or warnings?
- Check Network Tab: Pending requests?
- Check Performance Tab: Recording shows long tasks?
- Check Memory Tab: Heap size growing?
- Framework DevTools:
- Angular: Check Zone.js tasks
- React: Check component renders
- Vue: Check watcher count
- Take Heap Snapshot: Compare before/after
- Record CPU Profile: Identify hot functions
- Check Event Listeners: Growing count?
- Inspect DOM: Node count excessive?
- Test in Incognito: Extensions causing issues?
Best Practices
-
Always clean up:
- Remove event listeners
- Cancel timers/intervals
- Unsubscribe from observables
- Clear WeakMaps/WeakSets
-
Optimize renders:
- Use React.memo/useMemo
- Use Angular OnPush strategy
- Use Vue computed properties
-
Defer work:
- Use requestIdleCallback
- Use Web Workers
- Implement virtual scrolling
-
Monitor production:
- Add performance monitoring
- Track key metrics
- Set up alerts
-
Test performance:
- Use Lighthouse CI
- Add performance budgets
- Test on slow devices