Senior performance engineer who identifies bottlenecks and applies targeted optimizations with measurable impact.
Use this skill when
- Diagnosing or improving Core Web Vitals (LCP, INP, CLS)
- Reducing JavaScript bundle size or optimizing code splitting
- Implementing caching at any layer (HTTP, CDN, Redis, application)
- Optimizing database queries or fixing N+1 problems
- Debugging memory leaks in Node.js or browser
- Designing async processing pipelines for throughput
Core Web Vitals
LCP (Largest Contentful Paint) — Target: < 2.5s
LCP measures when the largest visible element finishes rendering. Usually a hero image, video, or large text block.
Common fixes:
- Preload the LCP resource:
<link rel="preload" as="image" href="/hero.webp"> - Inline critical CSS, defer the rest. Use
crittersfor automated critical CSS extraction. - Set
fetchpriority="high"on the LCP image. Removeloading="lazy"from above-the-fold images. - Serve from CDN. Eliminate redirect chains.
- Use
103 Early Hintsto let the browser start fetching before HTML arrives.
<!-- Optimal LCP image -->
<img
src="/hero.webp"
srcset="/hero-480.webp 480w, /hero-800.webp 800w, /hero-1200.webp 1200w"
sizes="(max-width: 600px) 480px, (max-width: 1024px) 800px, 1200px"
width="1200"
height="630"
alt="Hero image"
fetchpriority="high"
decoding="async"
/>
INP (Interaction to Next Paint) — Target: < 200ms
INP replaced FID in March 2024. It measures the worst-case delay between user input and visual update across the entire page lifecycle.
Common fixes:
- Break long tasks (>50ms) with
scheduler.yield()orsetTimeout(0). - Move heavy computation to Web Workers.
- Debounce rapid-fire events (scroll, resize, input) — 100-150ms debounce.
- Avoid layout thrashing: batch DOM reads, then batch DOM writes.
- Use
content-visibility: autoon offscreen sections.
// Break a long task into yielding chunks
async function processItems(items: Item[]) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
if (i % 50 === 0) {
// Yield to main thread every 50 items
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
CLS (Cumulative Layout Shift) — Target: < 0.1
Common fixes:
- Always set explicit
widthandheighton images and videos (browser calculates aspect ratio). - Reserve space for dynamic content:
min-heighton containers that load async. - Never inject content above existing content unless triggered by user interaction.
- Use
font-display: optionalor preload fonts to prevent FOIT/FOUT shifts. - Use CSS
aspect-ratiofor responsive embeds:aspect-ratio: 16 / 9.
Bundle Analysis and Code Splitting
Analyze First
# Next.js — built-in analyzer
ANALYZE=true next build
# Webpack
npx webpack-bundle-analyzer dist/stats.json
# Vite
npx vite-bundle-visualizer
Look for: duplicate dependencies, moment.js (replace with dayjs), lodash (use lodash-es or per-function imports), large polyfills.
Code Splitting Patterns
// Route-based splitting (React)
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
// Component-level splitting for heavy UI
const HeavyChart = lazy(() => import("./components/HeavyChart"));
function App() {
return (
<Suspense fallback={<Skeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Prefetch on hover for perceived instant loading
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const prefetch = () => {
if (to === "/dashboard") import("./pages/Dashboard");
};
return <Link to={to}
}
Tree Shaking Essentials
- Use ESM (
import/export), not CJS (require). CJS is not tree-shakeable. - Set
"sideEffects": falsein package.json (or list files with side effects). - Avoid barrel files (
index.tsre-exporting everything) — they defeat tree shaking.
Image Optimization
Format Selection
| Format | Use for | Browser support |
|---|---|---|
| AVIF | Photos, complex images. 50% smaller than JPEG. | Chrome, Firefox, Safari 16.4+ |
| WebP | Universal fallback. 30% smaller than JPEG. | All modern browsers |
| SVG | Icons, logos, simple graphics. Infinite scale. | Universal |
| PNG | Screenshots with text, transparency needed. | Universal |
Next.js Image Component
import Image from "next/image";
// Automatically serves AVIF > WebP > JPEG, responsive sizes, lazy loaded
<Image
src="/product.jpg"
width={800}
height={600}
alt="Product photo"
sizes="(max-width: 768px) 100vw, 50vw"
priority={isAboveFold} // Sets fetchpriority="high", disables lazy load
/>
Sharp for Server-Side Processing
import sharp from "sharp";
await sharp(inputBuffer)
.resize(1200, 630, { fit: "cover", position: "attention" }) // Smart crop
.avif({ quality: 50 }) // AVIF at quality 50 ≈ JPEG at quality 80
.toFile("output.avif");
Caching Strategies
HTTP Cache-Control Headers
# Static assets (hashed filenames): cache forever
Cache-Control: public, max-age=31536000, immutable
# API responses: revalidate after 60s, serve stale while revalidating
Cache-Control: public, max-age=60, stale-while-revalidate=300
# User-specific data: no shared cache
Cache-Control: private, max-age=0, must-revalidate
# Never cache
Cache-Control: no-store
Redis Caching Patterns
async function getCachedOrFetch<T>(
key: string,
fetcher: () => Promise<T>,
ttlSeconds: number
): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached) as T;
const data = await fetcher();
// SET with EX (expire) and NX (only if not exists) to prevent thundering herd
await redis.set(key, JSON.stringify(data), "EX", ttlSeconds);
return data;
}
// Cache-aside with stale-while-revalidate pattern
async function getWithSWR<T>(key: string, fetcher: () => Promise<T>, ttl: number): Promise<T> {
const cached = await redis.get(key);
if (cached) {
const parsed = JSON.parse(cached) as { data: T; fetchedAt: number };
const age = Date.now() - parsed.fetchedAt;
if (age > ttl * 500) {
// Over 50% of TTL — revalidate in background
fetcher().then((fresh) =>
redis.set(key, JSON.stringify({ data: fresh, fetchedAt: Date.now() }), "EX", ttl)
);
}
return parsed.data;
}
const data = await fetcher();
await redis.set(key, JSON.stringify({ data, fetchedAt: Date.now() }), "EX", ttl);
return data;
}
Database Query Optimization
Index Strategy
- Run
EXPLAIN ANALYZEon every slow query. Look forSeq Scanon large tables. - Index columns used in
WHERE,JOIN,ORDER BY. Composite indexes: leftmost prefix matters. - Covering indexes: include all
SELECTcolumns so the DB reads only the index, not the table.
-- Composite index: supports WHERE user_id = ? AND created_at > ?
CREATE INDEX idx_orders_user_created ON orders (user_id, created_at DESC);
-- Covering index: query satisfied entirely from index
CREATE INDEX idx_orders_covering ON orders (user_id, created_at DESC)
INCLUDE (total_amount, status);
-- Partial index: only index rows that matter
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';
N+1 Detection and Fix
// BAD: N+1 — 1 query for users + N queries for orders
const users = await db.query("SELECT * FROM users LIMIT 100");
for (const user of users) {
user.orders = await db.query("SELECT * FROM orders WHERE user_id = $1", [user.id]);
}
// GOOD: 2 queries total with a join or IN clause
const users = await db.query("SELECT * FROM users LIMIT 100");
const userIds = users.map((u) => u.id);
const orders = await db.query("SELECT * FROM orders WHERE user_id = ANY($1)", [userIds]);
const ordersByUser = Map.groupBy(orders, (o) => o.user_id);
for (const user of users) {
user.orders = ordersByUser.get(user.id) ?? [];
}
Memory Leak Detection
Node.js
# Start with inspector
node --inspect dist/server.js
# Generate heap snapshot programmatically
kill -USR2 <pid> # Node writes .heapsnapshot to cwd
Common Node.js leak sources:
- Event listeners never removed. Use
AbortControllerto clean up. - Closures capturing large objects. Null out references after use.
- Global caches without eviction. Use
lru-cachewithmaxandttl. - Unreferenced timers. Always
clearInterval/clearTimeouton shutdown.
import { LRUCache } from "lru-cache";
// Bounded cache: max 1000 entries, 5-minute TTL
const cache = new LRUCache<string, object>({
max: 1000,
ttl: 1000 * 60 * 5,
});
Browser (Chrome DevTools)
- Open Memory tab. Take a heap snapshot (Snapshot 1).
- Perform the suspected leaking action (navigate, open modal, etc.).
- Take Snapshot 2. Use "Comparison" view to see allocated-but-not-freed objects.
- Look for
Detached HTMLDivElement— DOM nodes removed from tree but still referenced in JS. - Filter by "Objects allocated between Snapshot 1 and 2" to find the leak source.
Connection Pooling
// Go database/sql — pool is built in
db, err := sql.Open("postgres", connStr)
db.SetMaxOpenConns(25) // Match your DB's max_connections / number_of_instances
db.SetMaxIdleConns(10) // Keep idle connections warm
db.SetConnMaxLifetime(5 * time.Minute) // Rotate connections to rebalance after DB failover
db.SetConnMaxIdleTime(1 * time.Minute) // Close idle connections to free DB resources
// Node.js with pg pool
import { Pool } from "pg";
const pool = new Pool({
max: 20, // Max connections in pool
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 5000, // Fail fast if no connection available
});
// Always release connections — use pool.query() for auto-release
const result = await pool.query("SELECT * FROM users WHERE id = $1", [userId]);
Async Processing Patterns
When an API handler does work that isn't needed for the response, move it out of the request path.
// BAD: user waits for email sending + analytics
app.post("/signup", async (req, res) => {
const user = await createUser(req.body);
await sendWelcomeEmail(user); // 500ms
await trackSignupEvent(user); // 200ms
res.json(user); // Total: 700ms+ for user
});
// GOOD: respond immediately, process async via queue
app.post("/signup", async (req, res) => {
const user = await createUser(req.body);
await queue.add("send-welcome-email", { userId: user.id });
await queue.add("track-signup", { userId: user.id });
res.json(user); // Total: ~50ms for user
});
Use BullMQ (Node.js), Celery (Python), or a message broker (RabbitMQ, SQS) for production queues. Always make queue consumers idempotent — jobs may be retried.
Game Performance
Frame Budgets
| Target FPS | Frame Time | Platform |
|---|---|---|
| 30 FPS | 33.3 ms | Console (heavy games) |
| 60 FPS | 16.6 ms | PC, Console, Mobile |
| 90 FPS | 11.1 ms | VR (minimum) |
| 120 FPS | 8.3 ms | Competitive games |
| 144+ FPS | 6.9 ms | High-end PC |
At 60 FPS (16.6ms), budget roughly 8ms CPU (game logic, physics, animation, audio) and 8ms GPU (geometry, lighting, post-process, UI).
CPU Optimization
- Algorithmic: Spatial hashing for neighbor queries, early-out conditions, reduce O(n^2) to O(n log n).
- Cache-friendly data: Data-oriented design (Struct of Arrays over Array of Structs), process data linearly, minimize cache misses.
- Allocation: Object pooling, pre-allocate collections, avoid GC in hot paths.
- Threading: Offload to job systems, async loading, parallel processing.
GPU Optimization
- Draw calls: Static/dynamic batching, GPU instancing, merge meshes. Target <2000 on PC, <200 on mobile.
- Overdraw: Front-to-back rendering, occlusion culling, reduce transparency.
- Shaders: Reduce instruction count, use half precision, minimize texture samples.
- Geometry: LOD systems, mesh simplification, frustum culling.
Platform Targets
Mobile constraints:
- Thermal throttling and battery drain are primary limits
- Memory: 500MB-2GB. Draw calls: 100-200. Triangles: 100K-500K/frame. Texture memory: 200-500MB.
- Target 30-60 FPS stable.
VR requirements:
- Maintain 90 FPS constantly (dropped frames cause nausea).
- Single-pass stereo rendering, fixed foveated rendering, aggressive LOD, minimal post-processing.
Frame Rate Troubleshooting
- Frame drops: Profile CPU vs GPU bound. Check for GC spikes. Move logic to FixedUpdate or coroutines. Add object pooling and LOD.
- Long loads: Async loading with progress bar. Stream assets in background. Compress aggressively. Pre-warm caches.
- Inconsistent pacing: Enable VSync. Use fixed timestep for physics. Spread heavy work across frames.
Profiling Tools
| Engine | CPU Profiler | GPU Profiler | Memory |
|---|---|---|---|
| Unity | Profiler | Frame Debugger | Memory Profiler |
| Unreal | Insights | RenderDoc | Memreport |
| Godot | Profiler | GPU Debugger | Built-in |
| Any | Platform tools | RenderDoc/PIX | Valgrind/Instruments |