Performance Profiling Mastery: Find and Fix Bottlenecks in Production
Introduction
Performance problems frustrate users and cost revenue. This guide covers systematic performance profiling using Chrome DevTools for frontend analysis, Visual Studio Performance Profiler for .NET applications, Node.js profiling tools, dotnet-trace for production diagnostics, memory leak detection techniques, and optimization strategies based on real measurement data.
Chrome DevTools Performance Panel
Recording Performance
Basic Recording:
// Open DevTools → Performance tab
// Click Record (or Ctrl+E)
// Perform user actions
// Stop recording
// Or profile specific code:
console.profile('MyOperation');
performExpensiveOperation();
console.profileEnd('MyOperation');
Performance Marks and Measures:
// Mark important events
performance.mark('data-fetch-start');
await fetchData();
performance.mark('data-fetch-end');
// Measure duration
performance.measure('data-fetch', 'data-fetch-start', 'data-fetch-end');
// Get measurements
const measurements = performance.getEntriesByType('measure');
measurements.forEach(measure => {
console.log(`${measure.name}: ${measure.duration}ms`);
});
// Clear marks
performance.clearMarks();
performance.clearMeasures();
Analyzing Flame Charts
Reading the Flame Chart:
Main Thread Timeline:
├─ Task (400ms) [Long Task - Yellow]
│ ├─ parseHTML (50ms)
│ ├─ recalculateStyle (100ms) [⚠️ Expensive]
│ ├─ layout (120ms) [⚠️ Forced Reflow]
│ └─ paint (130ms)
├─ JavaScript (200ms)
│ ├─ onClick (5ms)
│ ├─ updateDOM (150ms) [⚠️ Performance Issue]
│ └─ React.render (45ms)
Identifying Issues:
// ❌ Causes layout thrashing (forced reflows)
for (let i = 0; i < items.length; i++) {
// Read - causes layout
const height = element.offsetHeight;
// Write - invalidates layout
items[i].style.height = height + 'px';
}
// ✅ Batch reads and writes
const heights = [];
// Batch all reads
for (let i = 0; i < items.length; i++) {
heights.push(element.offsetHeight);
}
// Batch all writes
for (let i = 0; i < items.length; i++) {
items[i].style.height = heights[i] + 'px';
}
Long Task Detection
Monitoring Long Tasks:
// Long tasks block main thread (>50ms)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`Long task detected: ${entry.duration}ms`, entry);
// Send to analytics
if (window.gtag) {
gtag('event', 'long_task', {
duration: entry.duration,
start_time: entry.startTime
});
}
}
});
observer.observe({ entryTypes: ['longtask'] });
Breaking Up Long Tasks:
// ❌ Blocks main thread for 500ms
function processLargeArray(items) {
items.forEach(item => {
expensiveOperation(item);
});
}
// ✅ Yield to browser between chunks
async function processLargeArrayChunked(items, chunkSize = 50) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
chunk.forEach(item => expensiveOperation(item));
// Yield to browser
await new Promise(resolve => setTimeout(resolve, 0));
}
}
Network Performance
Resource Timing:
// Analyze network requests
const resources = performance.getEntriesByType('resource');
// Find slow requests
const slowRequests = resources.filter(r => r.duration > 1000);
slowRequests.forEach(request => {
console.log(`Slow: ${request.name}`);
console.log(` Duration: ${request.duration}ms`);
console.log(` DNS: ${request.domainLookupEnd - request.domainLookupStart}ms`);
console.log(` TCP: ${request.connectEnd - request.connectStart}ms`);
console.log(` TTFB: ${request.responseStart - request.requestStart}ms`);
console.log(` Download: ${request.responseEnd - request.responseStart}ms`);
});
Optimizing Requests:
// ❌ Sequential requests
const user = await fetch('/api/user');
const orders = await fetch('/api/orders');
const products = await fetch('/api/products');
// ✅ Parallel requests
const [user, orders, products] = await Promise.all([
fetch('/api/user'),
fetch('/api/orders'),
fetch('/api/products')
]);
// ✅ Request prioritization
<link rel="preconnect" href="https://api.contoso.com" />
<link rel="dns-prefetch" href="https://cdn.contoso.com" />
<link rel="preload" href="/critical.css" as="style" />
<link rel="prefetch" href="/next-page.js" />
Visual Studio Performance Profiler
CPU Usage Profiling
Recording CPU Profile:
Debug → Performance Profiler (Alt+F2)
Select: CPU Usage
Start
Perform operations that are slow
Stop
Analyze:
- Hot Path (most expensive call stack)
- Functions view (time per function)
- Caller/Callee view
Example Analysis:
// Hot Path shows:
// ProcessOrders (100%) - 5000ms
// └─ CalculateDiscount (80%) - 4000ms
// └─ GetCustomerHistory (75%) - 3750ms
// └─ ExecuteQuery (70%) - 3500ms [⚠️ Database]
// Optimization:
// ❌ N+1 query problem
public decimal CalculateDiscount(Order order)
{
// Executes query for EACH order
var customerHistory = _db.Orders
.Where(o => o.CustomerId == order.CustomerId)
.ToList();
return customerHistory.Count > 10 ? 0.15m : 0m;
}
// ✅ Batch query with caching
private Dictionary<int, int> _orderCounts = new();
public void PreloadCustomerHistory(IEnumerable<int> customerIds)
{
_orderCounts = _db.Orders
.Where(o => customerIds.Contains(o.CustomerId))
.GroupBy(o => o.CustomerId)
.ToDictionary(g => g.Key, g => g.Count());
}
public decimal CalculateDiscount(Order order)
{
var orderCount = _orderCounts.GetValueOrDefault(order.CustomerId, 0);
return orderCount > 10 ? 0.15m : 0m;
}
Memory Usage Profiling
Heap Snapshots:
Performance Profiler → .NET Object Allocation Tracking
Start → Perform operations → Take Snapshot
Compare snapshots to find:
- Objects that aren't garbage collected (memory leaks)
- Large object allocations
- High allocation rates
Memory Leak Example:
// ❌ Memory leak - event not unsubscribed
public class OrderService
{
private readonly IEventBus _eventBus;
public OrderService(IEventBus eventBus)
{
_eventBus = eventBus;
_eventBus.OrderPlaced += OnOrderPlaced;
}
private void OnOrderPlaced(object sender, OrderEventArgs e)
{
// Process order
}
// Missing: Dispose/Unsubscribe
}
// ✅ Properly dispose
public class OrderService : IDisposable
{
private readonly IEventBus _eventBus;
public OrderService(IEventBus eventBus)
{
_eventBus = eventBus;
_eventBus.OrderPlaced += OnOrderPlaced;
}
public void Dispose()
{
_eventBus.OrderPlaced -= OnOrderPlaced;
}
}
Database Performance
Database tool:
Performance Profiler → Database
Shows:
- Query execution times
- Query counts
- Connection pool usage
Optimization:
// ❌ Multiple round trips
var users = await _db.Users.ToListAsync();
foreach (var user in users)
{
user.Orders = await _db.Orders
.Where(o => o.UserId == user.Id)
.ToListAsync();
}
// ✅ Single query with eager loading
var users = await _db.Users
.Include(u => u.Orders)
.ToListAsync();
// ✅ Projection for large datasets
var userSummaries = await _db.Users
.Select(u => new UserSummary
{
Id = u.Id,
Name = u.Name,
OrderCount = u.Orders.Count,
TotalSpent = u.Orders.Sum(o => o.Total)
})
.ToListAsync();
Node.js Profiling
V8 CPU Profiler
Built-in Profiler:
# Start Node.js with inspector
node --inspect app.js
# Generate CPU profile
node --prof app.js
# Process profile
node --prof-process isolate-0xXXXXXXXXXXXX-v8.log > profile.txt
Using clinic.js:
# Install
npm install -g clinic
# CPU profiling
clinic doctor -- node app.js
# Flame graph
clinic flame -- node app.js
# Bubble profiler
clinic bubbleprof -- node app.js
# Open report
clinic doctor --open
Memory Profiling
Heap Snapshots:
// Take heap snapshot programmatically
const v8 = require('v8');
const fs = require('fs');
function takeHeapSnapshot() {
const snapshotStream = v8.writeHeapSnapshot();
const timestamp = Date.now();
const filename = `heap-${timestamp}.heapsnapshot`;
console.log(`Writing heap snapshot to ${filename}`);
return filename;
}
// Monitor memory usage
setInterval(() => {
const usage = process.memoryUsage();
console.log('Memory Usage:', {
rss: `${Math.round(usage.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
external: `${Math.round(usage.external / 1024 / 1024)}MB`
});
// Alert on high memory
if (usage.heapUsed > 500 * 1024 * 1024) {
console.warn('High memory usage detected!');
takeHeapSnapshot();
}
}, 30000);
Memory Leak Detection:
# Install memlab
npm install -g memlab
# Run memory leak detection
memlab run --scenario leak-scenario.js
leak-scenario.js:
module.exports = {
url: () => 'http://localhost:3000',
action: async (page) => {
// Perform actions that might leak
await page.click('#load-data');
await page.waitForTimeout(1000);
},
back: async (page) => {
await page.click('#clear-data');
await page.waitForTimeout(1000);
}
};
Async Performance
async_hooks for tracking:
const async_hooks = require('async_hooks');
const fs = require('fs');
const activeRequests = new Map();
const hook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
if (type === 'PROMISE') {
activeRequests.set(asyncId, {
type,
triggerAsyncId,
stack: new Error().stack
});
}
},
destroy(asyncId) {
activeRequests.delete(asyncId);
}
});
hook.enable();
// Monitor long-running async operations
setInterval(() => {
console.log(`Active async operations: ${activeRequests.size}`);
if (activeRequests.size > 1000) {
console.warn('High number of pending async operations!');
}
}, 10000);
Production Profiling with dotnet-trace
Collecting Traces
Install dotnet-trace:
# Install globally
dotnet tool install --global dotnet-trace
# List running .NET processes
dotnet-trace ps
# Collect trace (60 seconds)
dotnet-trace collect --process-id 1234 --duration 00:00:60
# Collect specific events
dotnet-trace collect --process-id 1234 \
--providers Microsoft-Windows-DotNETRuntime:0x1F:4
Analyze in Visual Studio:
1. Open trace file in Visual Studio
2. Performance Profiler → Open
3. View CPU, memory, and GC events
4. Filter by time range or thread
Live Metrics
Using dotnet-counters:
# Install
dotnet tool install --global dotnet-counters
# Monitor live metrics
dotnet-counters monitor --process-id 1234
# Output:
# [System.Runtime]
# CPU Usage (%) 45
# GC Heap Size (MB) 512
# Gen 0 GC Count 123
# ThreadPool Thread Count 25
# Exception Count 2
Custom EventCounters:
public class OrderMetrics
{
private readonly EventCounter _ordersProcessed;
private readonly EventCounter _avgProcessingTime;
public OrderMetrics(EventSource eventSource)
{
_ordersProcessed = new EventCounter("orders-processed", eventSource);
_avgProcessingTime = new EventCounter("avg-processing-time", eventSource);
}
public void RecordOrder(TimeSpan processingTime)
{
_ordersProcessed.WriteMetric(1);
_avgProcessingTime.WriteMetric(processingTime.TotalMilliseconds);
}
}
// Monitor custom counters
dotnet-counters monitor --process-id 1234 --counters MyApp.OrderService
Memory Leak Detection
Browser Memory Leaks
Chrome DevTools Memory Profiler:
// Common leak: Detached DOM nodes
// ❌ Keeps reference to removed DOM
let cache = [];
function addItem() {
const div = document.createElement('div');
document.body.appendChild(div);
cache.push(div); // Leak: holds reference after removal
}
function removeItems() {
document.body.innerHTML = ''; // Removes from DOM
// cache still holds references!
}
// ✅ Clear references
function clearCache() {
cache = [];
}
// Common leak: Event listeners
// ❌ Listener not removed
element.addEventListener('click', handleClick);
element.remove(); // Leak: listener still attached
// ✅ Remove listeners
element.removeEventListener('click', handleClick);
element.remove();
Three Snapshot Technique:
1. Take heap snapshot (baseline)
2. Perform action that might leak
3. Take second snapshot
4. Perform action again
5. Take third snapshot
6. Compare: Objects that grew in both steps are leaks
.NET Memory Analysis
Using dotnet-dump:
# Capture dump
dotnet-dump collect --process-id 1234
# Analyze dump
dotnet-dump analyze dump_20240101_120000.dmp
# Commands in analyzer:
dumpheap -stat # Object statistics
dumpheap -mt 00007fff12345678 # Objects of specific type
gcroot 000001a2b3c4d5e6 # Find roots keeping object alive
eeheap -gc # GC heap stats
Optimization Strategies
Frontend Optimization
Code Splitting:
// ❌ Single bundle (500KB)
import { ComponentA, ComponentB, ComponentC } from './components';
// ✅ Dynamic imports (lazy loading)
const ComponentA = lazy(() => import('./ComponentA'));
const ComponentB = lazy(() => import('./ComponentB'));
const ComponentC = lazy(() => import('./ComponentC'));
<Suspense fallback={<Loading />}>
<ComponentA />
</Suspense>
Virtualization:
// ❌ Render 10,000 rows (slow)
{items.map(item => <Row key={item.id} data={item} />)}
// ✅ Virtual scrolling (fast)
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<Row style={style} data={items[index]} />
)}
</FixedSizeList>
Backend Optimization
Caching:
// Distributed cache
public async Task<User> GetUserAsync(int userId)
{
var cacheKey = $"user:{userId}";
// Try cache first
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<User>(cached);
}
// Cache miss - query database
var user = await _db.Users.FindAsync(userId);
// Store in cache (15 minutes)
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(user),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
});
return user;
}
Async Optimization:
// ❌ Blocking calls
public IActionResult GetDashboard()
{
var users = GetUsers().Result; // Blocks thread
var orders = GetOrders().Result; // Blocks thread
var products = GetProducts().Result; // Blocks thread
return View(new { users, orders, products });
}
// ✅ Parallel async
public async Task<IActionResult> GetDashboard()
{
var tasks = new[]
{
GetUsers(),
GetOrders(),
GetProducts()
};
await Task.WhenAll(tasks);
return View(new
{
users = tasks[0].Result,
orders = tasks[1].Result,
products = tasks[2].Result
});
}
Best Practices
- Measure Before Optimizing: Profile first, optimize based on data
- Focus on Hot Paths: Optimize code that runs most frequently
- Set Performance Budgets: Define acceptable thresholds (e.g., <3s load time)
- Monitor Production: Real user monitoring catches issues in production
- Automate Testing: Performance tests in CI/CD prevent regressions
- Cache Wisely: Balance cache hit rate vs memory usage
- Profile Regularly: Catch performance degradation early
Troubleshooting
High CPU Usage:
# Identify hot functions
dotnet-trace collect --process-id 1234
# Analyze flame graph in Visual Studio
Memory Growth:
# Take multiple heap snapshots
dotnet-dump collect --process-id 1234
# Analyze object growth between snapshots
Slow Page Load:
// Check Lighthouse metrics
npx lighthouse https://contoso.com --view
Key Takeaways
- Chrome DevTools Performance panel identifies frontend bottlenecks and long tasks
- Visual Studio Performance Profiler reveals CPU hotspots and memory leaks in .NET
- Node.js profiling with clinic.js and V8 inspector optimizes backend performance
- Production profiling with dotnet-trace diagnoses live issues without stopping services
- Memory leak detection requires systematic snapshot comparison techniques
Next Steps
- Implement Real User Monitoring (RUM) with Application Insights or Datadog
- Set up performance budgets in Lighthouse CI
- Create load testing scenarios with k6 or JMeter
- Establish performance SLOs for critical user journeys
Additional Resources
- Chrome DevTools Performance
- Visual Studio Performance Profiler
- Node.js Performance
- dotnet-trace Documentation
Measure, optimize, verify.