Performance Tips
Measure before you optimise
Guesswork optimisation wastes time and usually speeds up the wrong place. Without data you can't measure impact.
Lab measurement with Lighthouse / WebPageTest; RUM for real users — Web Vitals JS, Sentry Performance, New Relic Browser, Application Insights. Track p75 and p95.
p50 may look fast, but if p95 is slow, 5% of users have a poor experience.
Hit Core Web Vitals targets: LCP < 2.5s, INP < 200ms, CLS < 0.1
Google uses CWV as ranking signals and they directly reflect user perception. Missing thresholds hurts SEO and conversion.
For LCP, preload the hero image at correct size/format. For INP, keep main thread free of long tasks (web workers, code split). For CLS, set explicit dimensions on images/iframes, plan font swap.
<img width="1200" height="630" loading="eager" fetchpriority="high"> can drop hero LCP by 500ms.
Serve images in modern formats at correct size
Images are typically 50%+ of payload. AVIF/WebP shrink them 30–80% vs PNG/JPG. Downloading at wrong size and scaling via CSS wastes bandwidth.
<picture> with AVIF→WebP→JPEG fallback. srcset/sizes for responsive. loading="lazy" on below-the-fold images. CDN-based on-the-fly resize (Cloudflare Images, ImageKit, Cloudinary).
Next.js: <Image>; .NET: ImageSharp.Web with URL params (?width=800&format=avif) for on-demand resize.
Keep JavaScript bundle size in check
JS is downloaded, parsed, and executed — three costs. On a 3G mobile device, 200KB of JS means 4–6 seconds of CPU time.
Code splitting (route-based, dynamic import). Tree-shaking. Avoid unnecessary polyfills (modern target). Use bundle analyzer to find big modules. Count third-party scripts in your perf budget.
Target: critical JS < 100KB gzip. moment.js → date-fns/Day.js (~70KB savings). lodash → modular imports.
Set cache headers and CDN correctly
Serving static assets from the nearest edge and from cache eliminates RTTs. Wrong headers → re-download every time.
For hashed files (app.abc123.js): Cache-Control: public, max-age=31536000, immutable. For HTML: no-cache or short max-age + ETag. Enable CDN for all static assets.
Cloudflare/Bunny CDN: cache-everything rule + Cache-Control respected. ASP.NET Core: app.UseStaticFiles + ResponseCachingMiddleware.
Create the right database indexes
Without indexes, queries do full table scans; latency grows linearly with data and the product 'hits a wall' at scale.
Use EXPLAIN/EXPLAIN ANALYZE to read query plans. Index WHERE/JOIN/ORDER BY columns. Order matters in composite indexes; put high-cardinality first. Too many indexes hurt write-heavy tables — balance.
PostgreSQL: CREATE INDEX CONCURRENTLY ON orders(user_id, created_at DESC); — for a page listing a user's orders by date.
Detect and eliminate N+1 query problems
ORMs sometimes issue N+1 queries instead of 1. 101 queries for 100 users on a page — devastating for DB and latency.
EF Core: .Include / .ThenInclude / projection. Prisma: include / select. Dapper: combine queries. Enable query logging in dev and measure critical pages.
EF Core: db.Orders.Include(o => o.Items).ThenInclude(i => i.Product) — fetch in a single SQL.
Enable compression (Brotli/gzip) and HTTP/2 or HTTP/3
Brotli shrinks HTML/JS/CSS by 15–25%. HTTP/2 multiplexing fixes head-of-line blocking; HTTP/3 (QUIC) tolerates packet loss on mobile.
Enable Brotli at server/CDN level. Don't re-compress already-compressed images. ASP.NET Core: app.UseResponseCompression with Brotli/Gzip providers.
Cloudflare offers automatic Brotli. Behind Kestrel/IIS, Response Compression Middleware is typical.
Optimise font loading
Web fonts can block render and cause CLS. Wrong loading produces FOIT (invisible text) or late swap.
Use WOFF2. Subset to the weights/character sets you use. font-display: swap. Preload critical font with <link rel="preload">. Variable fonts pack multiple weights in one file.
Inter Variable Latin subset (~30KB) covers all weights; self-hosting beats Google Fonts on speed + is GDPR-friendly.
Inline critical CSS, lazy-load the rest
Render-blocking CSS delays LCP. Inlining the minimum CSS for above-the-fold makes first paint instant.
Extract critical CSS with Critical/critters/penthouse. Load the rest async via media trick: <link rel="preload" as="style" onload="this.rel='stylesheet'">.
Tailwind: JIT mode generates only used classes; per-page CSS splitting helps further.
Manage third-party scripts with a budget
Analytics, chat widgets, A/B testing — each adds 50–300KB and many long tasks. Unmanaged, they can halve homepage speed.
Only load what's truly needed. Use async/defer. When possible, offload to a worker via Partytown. Measure with Lighthouse 'third-party-summary'.
Session replay tools like Hotjar/FullStory degrade INP heavily; sampling at 5–10% usually gives enough signal.
Mind connection pooling and concurrency limits
Opening a fresh DB connection per request is expensive; if the pool fills, requests queue and slowdowns cascade.
Tune connection pool size by CPU/RAM. Use a singleton HttpClient for outbound APIs (IHttpClientFactory). Keep async/await chain unbroken.
.NET: services.AddHttpClient<ApiClient>(); — handler pool automatic. EF Core: Pooling=true (default), Max Pool Size in connection string.
Stream large responses; don't buffer them in memory
Buffering large JSON/files in memory adds GC pressure and uses RAM; under high concurrency this risks OOM.
ASP.NET Core: return IAsyncEnumerable, use FileStreamResult. Node: response.pipe(stream). Push big import/export jobs to a queue.
10K-row CSV export → background job + signed URL for the result; don't do it inline over HTTP.
Cache hot paths and plan invalidation
Running the same query 1000x/sec is wasteful. Cache yields 10–100x speedups, but 'cache invalidation' is one of the two hardest problems in software.
Read-heavy, rarely changing data: cache with Redis/Memcached. Design cache keys carefully (include a version). Combine TTL + event-based invalidation. Use stale-while-revalidate.
.NET 9+: HybridCache (memory + distributed) under one API. CacheKey: "user:{id}:profile:v2". Watch for memory leaks in long-lived apps
Server apps run for weeks. A slow leak (event listener, static collection, IDisposable) eventually forces restart or OOMs.
Compare heap snapshots (dotnet-counters, VS Diagnostic Tools, Chrome DevTools). Wrap IDisposables with using. If a code path appends to static collections, use a bounded ConcurrentDictionary.
ASP.NET: ConfigureAwait isn't needed (no sync context), but pass cancellation tokens through every async path.