
Performance
- 177 installs
- 213 repo stars
- Updated August 4, 2026
- yonatangross/orchestkit
Profile OrchestKit services, reduce API latency, and tune async orchestration paths so agent pipelines meet throughput and SLO targets before release.
About
OrchestKit performance skill guides profiling and optimization of Python backend and orchestration services, covering async workloads, caching, concurrency, and database tuning so agent and API products hit latency and throughput targets before shipping.
- Latency and throughput profiling
- Async pipeline optimization
- Caching and batching strategies
- Database and I/O tuning
- SLO-driven release gates
Performance by the numbers
- 177 all-time installs (skills.sh)
- +4 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #2,232 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yonatangross/orchestkit --skill performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 177 |
|---|---|
| repo stars | ★ 213 |
| Last updated | August 4, 2026 |
| Repository | yonatangross/orchestkit ↗ |
What it does
Profile OrchestKit services, reduce API latency, and tune async orchestration paths so agent pipelines meet throughput and SLO targets before release.
Files
Performance
Comprehensive performance optimization patterns for frontend, backend, and LLM inference.
Quick Reference
| Category | Rules | Impact | When to Use |
|---|---|---|---|
| Core Web Vitals | 4 | CRITICAL | LCP, INP, CLS optimization with 2026 thresholds |
| Render Optimization | 3 | HIGH | React Compiler, memoization, virtualization |
| Lazy Loading | 3 | HIGH | Code splitting, route splitting, preloading |
| Image Optimization | 3 | HIGH | Next.js Image, AVIF/WebP, responsive images |
| Profiling & Backend | 3 | MEDIUM | React DevTools, py-spy, bundle analysis |
| LLM Inference | 3 | MEDIUM | vLLM, quantization, speculative decoding |
| Caching | 2 | HIGH | Redis cache-aside, prompt caching, HTTP cache headers |
| Query & Data Fetching | 2 | HIGH | TanStack Query prefetching, optimistic updates, rollback |
| Sustainability | 1 | MEDIUM | Page weight budgets, lazy loading, optimized formats, dark mode |
Total: 24 rules across 9 categories
Core Web Vitals
Google's Core Web Vitals with 2026 stricter thresholds.
| Rule | File | Key Pattern |
|---|---|---|
| LCP Optimization | rules/cwv-lcp.md | Preload hero, SSR, fetchpriority="high" |
| INP Optimization | rules/cwv-inp.md | scheduler.yield, useTransition, requestIdleCallback |
| INP Advanced | rules/cwv-inp-advanced.md | Layout thrashing, third-party scripts, rAF patterns |
| CLS Prevention | rules/cwv-cls.md | Explicit dimensions, aspect-ratio, font-display |
2026 Thresholds
| Metric | Current Good | 2026 Good |
|---|---|---|
| LCP | <= 2.5s | <= 2.0s |
| INP | <= 200ms | <= 150ms |
| CLS | <= 0.1 | <= 0.08 |
Render Optimization
React render performance patterns for React 19+.
| Rule | File | Key Pattern |
|---|---|---|
| React Compiler | rules/render-compiler.md | Auto-memoization, "Memo" badge verification |
| Manual Memoization | rules/render-memo.md | useMemo/useCallback escape hatches, state colocation |
| Virtualization | rules/render-virtual.md | TanStack Virtual for 100+ item lists |
Lazy Loading
Code splitting and lazy loading with React.lazy and Suspense.
| Rule | File | Key Pattern |
|---|---|---|
| React.lazy + Suspense | rules/loading-lazy.md | Component lazy loading, error boundaries |
| Route Splitting | rules/loading-splitting.md | React Router 7.x, Vite manual chunks |
| Preloading | rules/loading-preload.md | Prefetch on hover, modulepreload hints |
Image Optimization
Production image optimization for modern web applications.
| Rule | File | Key Pattern |
|---|---|---|
| Next.js Image | rules/images-nextjs.md | Image component, priority, blur placeholder |
| Format Selection | rules/images-formats.md | AVIF/WebP, quality 75-85, picture element |
| Responsive Images | rules/images-responsive.md | sizes prop, art direction, CDN loaders |
Profiling & Backend
Profiling tools and backend optimization patterns.
| Rule | File | Key Pattern |
|---|---|---|
| React Profiling | rules/profiling-react.md | DevTools Profiler, flamegraph, render counts |
| Backend Profiling | rules/profiling-backend.md | py-spy, cProfile, memory_profiler, flame graphs |
| Bundle Analysis | rules/profiling-bundle.md | vite-bundle-visualizer, tree shaking, performance budgets |
LLM Inference
High-performance LLM inference with vLLM, quantization, and speculative decoding.
| Rule | File | Key Pattern |
|---|---|---|
| vLLM Deployment | rules/inference-vllm.md | PagedAttention, continuous batching, tensor parallelism |
| Quantization | rules/inference-quantization.md | AWQ, GPTQ, FP8, INT8 method selection |
| Speculative Decoding | rules/inference-speculative.md | N-gram, draft model, 1.5-2.5x throughput |
Caching
Backend Redis caching and LLM prompt caching for cost savings and performance.
| Rule | File | Key Pattern |
|---|---|---|
| Redis & Backend | rules/caching-redis.md | Cache-aside, write-through, invalidation, stampede prevention |
| HTTP & Prompt | rules/caching-http.md | HTTP cache headers, LLM prompt caching, semantic caching |
Query & Data Fetching
TanStack Query v5 patterns for prefetching and optimistic updates.
| Rule | File | Key Pattern |
|---|---|---|
| Prefetching | rules/query-prefetching.md | Hover prefetch, route loaders, queryOptions, Suspense |
| Optimistic Updates | rules/query-optimistic.md | Optimistic mutations, rollback, cache invalidation |
Sustainability
Digital sustainability patterns for reducing carbon footprint and energy usage.
| Rule | File | Key Pattern |
|---|---|---|
| Sustainability UX | rules/sustainability-ux.md | Page weight budgets, AVIF/WebP, lazy loading, dark mode |
Local Profiling Target
When profiling a local app (Lighthouse, Core Web Vitals, bundle analysis), use Portless named URLs for stable, self-documenting targets:
# Discover services
portless list
# app → app.localhost:1355 (port 3000)
# Profile with agent-browser (preferred for visual metrics)
agent-browser open "http://app.localhost:1355"
agent-browser profiler start
agent-browser wait --load networkidle
agent-browser profiler stop /tmp/profile.json
# Lighthouse via agent-browser
agent-browser open "http://app.localhost:1355"
agent-browser screenshot /tmp/perf-baseline.png
# Or direct Lighthouse CLI
npx lighthouse http://app.localhost:1355 --output=json --output-path=/tmp/lighthouse.jsonNamed URLs are stable across restarts and self-documenting in performance reports. Install Portless with npm i -g portless.
Quick Start Example
// LCP: Priority hero image with SSR
import Image from 'next/image';
export default async function Page() {
const data = await fetchHeroData();
return (
<Image
src={data.heroImage}
alt="Hero"
priority
placeholder="blur"
sizes="100vw"
fill
/>
);
}Key Decisions
| Decision | Recommendation |
|---|---|
| Memoization | Let React Compiler handle it (2026 default) |
| Lists 100+ items | Use TanStack Virtual |
| Image format | AVIF with WebP fallback (30-50% smaller) |
| LCP content | SSR/SSG, never client-side fetch |
| Code splitting | Per-route for most apps, per-component for heavy widgets |
| Prefetch strategy | On hover for nav links, viewport for content |
| Quantization | AWQ for 4-bit, FP8 for H100/H200 |
| Bundle budget | Hard fail in CI to prevent regression |
Common Mistakes
1. Client-side fetching LCP content (delays render) 2. Images without explicit dimensions (causes CLS) 3. Lazy loading LCP images (delays largest paint) 4. Heavy computation in event handlers (blocks INP) 5. Layout-shifting animations (use transform instead) 6. Lazy loading tiny components < 5KB (overhead > savings) 7. Missing error boundaries on lazy components 8. Using GPTQ without calibration data 9. Not benchmarking actual workload patterns 10. Only measuring in lab environment (need RUM)
Related Skills
ork:react-server-components-framework- Server-first renderingork:vite-advanced- Build optimizationbrowser-tools- Visual profiling with agent-browser + Portlesscaching- Cache strategies for responsesork:monitoring-observability- Production monitoring and alertingork:database-patterns- Query and index optimizationork:llm-integration- Local inference with Ollama
Capability Details
lcp-optimization
Keywords: LCP, largest-contentful-paint, hero, preload, priority, SSR Solves:
- Optimize hero image loading
- Server-render critical content
- Preload and prioritize LCP resources
inp-optimization
Keywords: INP, interaction, responsiveness, long-task, transition, yield Solves:
- Break up long tasks with scheduler.yield
- Defer non-urgent updates with useTransition
- Optimize event handler performance
cls-prevention
Keywords: CLS, layout-shift, dimensions, aspect-ratio, font-display Solves:
- Reserve space for dynamic content
- Prevent font flash and image pop-in
- Use transform for animations
react-compiler
Keywords: react-compiler, auto-memo, memoization, React 19 Solves:
- Enable automatic memoization
- Identify when manual memoization needed
- Verify compiler is working
virtualization
Keywords: virtual, TanStack, large-list, scroll, overscan Solves:
- Render 100+ item lists efficiently
- Dynamic height virtualization
- Window scrolling patterns
lazy-loading
Keywords: React.lazy, Suspense, code-splitting, dynamic-import Solves:
- Route-based code splitting
- Component lazy loading with error boundaries
- Prefetch on hover and viewport
image-optimization
Keywords: next/image, AVIF, WebP, responsive, blur-placeholder Solves:
- Next.js Image component patterns
- Format selection and quality settings
- Responsive sizing and CDN configuration
profiling
Keywords: profiler, flame-graph, py-spy, DevTools, bundle-analyzer Solves:
- Profile React renders and backend code
- Generate and interpret flame graphs
- Analyze and optimize bundle size
inp-advanced
Keywords: INP, scheduler-yield, layout-thrashing, third-party-scripts, requestAnimationFrame Solves:
- Break long tasks with scheduler.yield()
- Audit and defer blocking third-party scripts
- Avoid synchronous layout thrashing in event handlers
- Optimize form submissions, dropdowns, accordions, filters
sustainability
Keywords: sustainability, carbon-footprint, page-weight, green-ux, dark-mode, lazy-loading Solves:
- Enforce page weight budgets (< 1MB)
- Eliminate auto-playing videos and heavy decorative animations
- Serve optimized image formats (AVIF/WebP)
- Implement cursor-based pagination to prevent over-fetching
llm-inference
Keywords: vllm, quantization, speculative-decoding, inference, throughput Solves:
- Deploy LLMs with vLLM for production
- Choose quantization method for hardware
- Accelerate generation with speculative decoding
References
Load on demand with Read("${CLAUDE_SKILL_DIR}/references/<file>"):
| File | Content |
|---|---|
rum-setup.md | Real User Monitoring |
react-compiler-migration.md | Compiler adoption |
tanstack-virtual-patterns.md | Virtualization patterns |
vllm-deployment.md | Production vLLM config |
quantization-guide.md | Method comparison |
cdn-setup.md | Image CDN configuration |
cc-prompt-cache-guide.md | CC 2.1.72 prompt cache optimization, stable-first prompt structure |
Core Web Vitals Optimization Checklist
Comprehensive checklist for achieving and maintaining good Core Web Vitals scores.
Thresholds Reference
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP | ≤ 2.5s | ≤ 4.0s | > 4.0s |
| INP | ≤ 200ms | ≤ 500ms | > 500ms |
| CLS | ≤ 0.1 | ≤ 0.25 | > 0.25 |
2026 Stricter Thresholds (plan ahead!):
- LCP: ≤ 2.0s
- INP: ≤ 150ms
- CLS: ≤ 0.08
---
LCP (Largest Contentful Paint) ≤ 2.5s
Identify the LCP Element
- [ ] Run Lighthouse to identify LCP element
- [ ] Use Performance Observer to confirm in production
- [ ] LCP is typically: hero image, hero heading, or above-the-fold banner
// Debug: Find LCP element
new PerformanceObserver((list) => {
const entries = list.getEntries();
console.log('LCP element:', entries[entries.length - 1].element);
}).observe({ type: 'largest-contentful-paint', buffered: true });Server Response Time (TTFB)
- [ ] Server response time (TTFB) < 800ms
- [ ] Use edge/CDN for static content
- [ ] Enable HTTP/2 or HTTP/3
- [ ] Compress responses (gzip/brotli)
- [ ] Database queries optimized
- [ ] Caching strategy implemented (Redis, CDN cache)
Critical Resource Loading
- [ ] LCP image has
fetchpriority="high"attribute - [ ] LCP image has
loading="eager"(not lazy) - [ ] LCP image preloaded in
<head> - [ ] Critical CSS inlined or preloaded
- [ ] Font preloaded with
crossoriginattribute - [ ] Preconnect to critical third-party origins
<!-- Preload critical resources -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<link rel="preload" as="font" href="/font.woff2" type="font/woff2" crossorigin />
<link rel="preconnect" href="https://api.example.com" />Image Optimization
- [ ] LCP image in modern format (WebP/AVIF)
- [ ] Image properly sized (not oversized)
- [ ] Responsive images with
srcset - [ ] Image CDN used (Cloudinary, imgix, Vercel)
Rendering Strategy
- [ ] LCP content rendered server-side (SSR/SSG)
- [ ] LCP content NOT loaded client-side via fetch
- [ ] No render-blocking JavaScript
- [ ] No render-blocking CSS below the fold
- [ ] Third-party scripts deferred
// ✅ GOOD: Server-rendered LCP content
export default async function Page() {
const hero = await getHeroData();
return <Hero data={hero} />;
}
// ❌ BAD: Client-loaded LCP content
function Page() {
const [hero, setHero] = useState(null);
useEffect(() => { fetchHero().then(setHero); }, []); // Delays LCP!
}---
INP (Interaction to Next Paint) ≤ 200ms
Identify Long Tasks
- [ ] Chrome DevTools Performance tab analyzed
- [ ] Long tasks (>50ms) identified
- [ ] Main thread blockers removed/optimized
JavaScript Optimization
- [ ] Heavy computation moved to Web Workers
- [ ] Large arrays processed in chunks with yielding
- [ ]
requestIdleCallbackused for non-critical work - [ ] Bundle size minimized (code splitting)
- [ ] Tree shaking enabled
// ✅ GOOD: Yield to main thread
async function processItems(items: Item[]) {
for (const item of items) {
processItem(item);
// Yield every 4ms to allow paint
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
}
}React Optimization
- [ ]
useTransitionfor non-urgent updates - [ ]
useDeferredValuefor expensive derivations - [ ] Memoization where appropriate (
useMemo,memo) - [ ] Virtualization for long lists (
react-window,@tanstack/virtual) - [ ] Suspense boundaries for code splitting
// ✅ GOOD: Non-blocking state updates
const [isPending, startTransition] = useTransition();
function handleSearch(query: string) {
setQuery(query); // Urgent: update input
startTransition(() => {
setFilteredResults(filter(query)); // Non-urgent: defer
});
}Event Handler Optimization
- [ ] No heavy computation in event handlers
- [ ] Handlers don't cause layout thrashing
- [ ] Passive event listeners for scroll/touch
- [ ] Debounced input handlers where appropriate
// ✅ GOOD: Defer heavy work
onClick={() => {
setLoading(true);
startTransition(() => {
const result = heavyComputation();
setResult(result);
setLoading(false);
});
}}
// ❌ BAD: Blocking handler
onClick={() => {
const result = heavyComputation(); // Blocks paint!
setResult(result);
}}Animation Performance
- [ ] Animations use
transformandopacityonly - [ ] No animations on layout properties (width, height, top, left)
- [ ]
will-changeused sparingly - [ ] Animations run at 60fps (checked in DevTools)
---
CLS (Cumulative Layout Shift) ≤ 0.1
Image Dimensions
- [ ] ALL images have explicit
widthandheight - [ ] Responsive images use
aspect-ratiocontainer - [ ]
fillprop images have sized container - [ ] No images cause layout shift on load
// ✅ GOOD: Explicit dimensions
<img src="/photo.jpg" width={800} height={600} alt="Photo" />
// ✅ GOOD: Aspect ratio container
<div className="aspect-[16/9]">
<Image src="/photo.jpg" fill alt="Photo" />
</div>Dynamic Content
- [ ] Space reserved for dynamic content (ads, embeds)
- [ ] Skeleton loaders match final content size
- [ ] No content inserted above existing content
- [ ] Lazy-loaded content has reserved space
// ✅ GOOD: Reserved space
<div className="min-h-[250px]">
{ad ? <Ad data={ad} /> : <Skeleton height={250} />}
</div>Font Loading
- [ ]
font-display: optionalorswapused - [ ] Fallback font has
size-adjustto match - [ ] Critical font preloaded
- [ ] System font stack as fallback
/* Fallback with size adjustment */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}Animation Stability
- [ ] Animations use
transform, not layout properties - [ ] Expanding/collapsing uses
scaleY, notheight - [ ] Modals/overlays don't shift page content
- [ ] Toast notifications positioned fixed/absolute
/* ✅ GOOD: Transform-based animation */
.drawer {
transform: translateX(-100%);
transition: transform 0.3s;
}
.drawer.open {
transform: translateX(0);
}
/* ❌ BAD: Layout-shifting animation */
.drawer {
width: 0;
transition: width 0.3s;
}Iframes and Embeds
- [ ] Iframes have explicit dimensions
- [ ] Third-party embeds wrapped with sized container
- [ ] Lazy iframes have placeholder
---
Measurement & Monitoring
Lab Testing
- [ ] Lighthouse CI in build pipeline
- [ ] Performance budgets enforced
- [ ] Regular manual Lighthouse audits
- [ ] Testing on throttled CPU/network
Field Data (RUM)
- [ ]
web-vitalslibrary installed - [ ] Metrics sent to analytics endpoint
- [ ] p75 percentile tracked (Google's standard)
- [ ] Alerts configured for regressions
// Essential RUM setup
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);Data Analysis
- [ ] Dashboard showing daily/weekly trends
- [ ] Segmentation by page, device, connection
- [ ] Comparison of lab vs field data
- [ ] Week-over-week regression detection
Alerting
- [ ] Alert when p75 exceeds threshold
- [ ] Alert when good rate drops below 75%
- [ ] Alert on significant week-over-week regression
- [ ] Escalation path defined
---
Build & Deploy
Performance Budgets
- [ ] Bundle size limits configured
- [ ] Build fails on budget exceeded
- [ ] Per-route budgets for large apps
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 150000, // 150KB
maxEntrypointSize: 250000, // 250KB
hints: 'error', // Fail build
},
};CI/CD Integration
- [ ] Lighthouse CI runs on PRs
- [ ] Performance regression blocks merge
- [ ] Bundle analyzer report generated
- [ ] Preview deployments for testing
CDN & Caching
- [ ] Static assets on CDN
- [ ] Immutable caching for hashed assets
- [ ] Stale-while-revalidate for HTML
- [ ] Edge caching where appropriate
---
Debugging Checklist
Slow LCP
- [ ] Check TTFB (server response time)
- [ ] Verify LCP element has
fetchpriority="high" - [ ] Confirm LCP content is server-rendered
- [ ] Check for render-blocking resources
- [ ] Verify image is optimized and properly sized
High INP
- [ ] Run Performance recording during interaction
- [ ] Look for long tasks in flame chart
- [ ] Check for forced synchronous layouts
- [ ] Verify heavy work is deferred
- [ ] Check for excessive re-renders
High CLS
- [ ] Run Lighthouse with "Layout Shift Regions" enabled
- [ ] Check images for missing dimensions
- [ ] Look for late-loading content
- [ ] Verify fonts have fallbacks
- [ ] Check for content inserted above viewport
---
Testing Protocol
Before Deployment
- [ ] Lighthouse score ≥ 90 on Performance
- [ ] All Core Web Vitals in "good" range
- [ ] No performance budget violations
- [ ] Tested on throttled 4G + slow CPU
After Deployment
- [ ] Monitor RUM for 24-48 hours
- [ ] Compare p75 to pre-deployment baseline
- [ ] Check for unexpected regressions
- [ ] Verify alerting is working
Weekly Review
- [ ] Review p75 trends
- [ ] Identify worst-performing pages
- [ ] Check for new issues in CrUX
- [ ] Plan optimizations for next sprint
Image Optimization Checklist
Comprehensive checklist for production-ready image optimization.
Format Selection
Photo Content
- [ ] Use AVIF as primary format (30-50% smaller than JPEG)
- [ ] Configure WebP as fallback for older browsers
- [ ] JPEG only for browsers without AVIF/WebP support
- [ ] Configure Next.js:
formats: ['image/avif', 'image/webp']
Graphics & Icons
- [ ] SVG for logos, icons, and simple graphics
- [ ] PNG only when transparency is required
- [ ] Consider SVG sprites for icon sets (reduces requests)
- [ ] Inline small SVGs (< 1KB) to avoid network requests
Format Decision Tree
Is it a photo/complex image?
├── Yes → Use AVIF/WebP (Next.js Image handles this)
└── No → Is transparency needed?
├── Yes → PNG or SVG
└── No → Is it an icon/logo?
├── Yes → SVG (scalable, tiny file size)
└── No → AVIF/WebP---
Dimensions & Sizing
Always Set Dimensions
- [ ] Every
<Image>haswidthandheightOR usesfill - [ ] Fill mode images have sized container (relative + dimensions)
- [ ] Dimensions match actual display size (not larger)
- [ ] No CLS from images (Layout Shift score = 0)
// ✅ GOOD: Explicit dimensions
<Image src="/photo.jpg" width={800} height={600} />
// ✅ GOOD: Fill with sized container
<div className="relative h-[400px]">
<Image src="/photo.jpg" fill />
</div>
// ❌ BAD: Missing dimensions
<Image src="/photo.jpg" />Responsive Images
- [ ]
sizesprop set for all responsive images - [ ] Sizes match actual layout breakpoints
- [ ] Don't serve images larger than needed
- [ ] Test with DevTools Network tab (check actual sizes served)
// ✅ GOOD: Accurate sizes prop
<Image
src="/photo.jpg"
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
// Common sizes patterns:
// Full width hero: sizes="100vw"
// Half width on desktop: sizes="(max-width: 768px) 100vw, 50vw"
// Grid of 4: sizes="(max-width: 640px) 50vw, 25vw"---
Loading Strategy
LCP Images (Above the Fold)
- [ ] Hero/banner image has
priorityprop - [ ] ONLY one image per page has
priority(usually LCP element) - [ ] LCP image preloaded in
<head>if not using Next.js Image - [ ] No lazy loading on LCP images
// ✅ GOOD: Priority on LCP image
<Image src="/hero.jpg" priority fill sizes="100vw" />
// ❌ BAD: Priority on all images
{images.map(img => <Image src={img} priority />)} // Wrong!Below-the-Fold Images
- [ ] Default lazy loading (Next.js Image default)
- [ ] No
priorityprop on non-LCP images - [ ] Consider
loading="lazy"for native<img>elements - [ ] Use Intersection Observer for custom lazy loading
Preloading
- [ ] Critical hero image preloaded
- [ ] Don't preload below-fold images
- [ ] Use
fetchpriority="high"for critical images
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />---
Placeholders
Blur Placeholders
- [ ] Static imports use
placeholder="blur"(automatic) - [ ] Remote images have
blurDataURLgenerated - [ ] Placeholder improves perceived performance
- [ ] Consider plaiceholder library for build-time generation
// ✅ Static import with automatic blur
import heroImage from '@/public/hero.jpg';
<Image src={heroImage} placeholder="blur" />
// ✅ Remote image with blur
<Image
src="https://cdn.example.com/photo.jpg"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>Color Placeholders
- [ ] Consider dominant color placeholder for cards
- [ ] Skeleton placeholders for loading states
- [ ] Smooth transition from placeholder to image
---
Quality Settings
Compression
- [ ] Quality set to 75-85 (not 100)
- [ ] Test quality visually - often 75 is indistinguishable
- [ ] Higher quality (85-90) only for hero/product images
- [ ] Lower quality (60-70) acceptable for thumbnails
// ✅ GOOD: Appropriate quality
<Image src="/hero.jpg" quality={85} /> // Important hero
<Image src="/thumbnail.jpg" quality={70} /> // Small thumbnail
// ❌ BAD: Unnecessary quality
<Image src="/photo.jpg" quality={100} /> // Huge file, no benefitAVIF-Specific
- [ ] AVIF quality can be 10-15 points lower than JPEG
- [ ] Test AVIF vs WebP on your content type
- [ ] Some images compress better with WebP
---
CDN & Infrastructure
Next.js Configuration
- [ ]
remotePatternsconfigured for all external domains - [ ]
deviceSizesmatches your breakpoints - [ ]
formatsincludes AVIF and WebP - [ ]
minimumCacheTTLset appropriately (30+ days for static)
// next.config.js
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{ hostname: 'cdn.example.com' },
{ hostname: '*.cloudinary.com' },
],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
}CDN Setup
- [ ] Images served from CDN (not origin server)
- [ ] Edge caching enabled
- [ ] Cache headers set correctly (1 year for hashed assets)
- [ ]
Vary: Acceptheader for format negotiation
Self-Hosted
- [ ] Sharp installed:
npm install sharp - [ ] Docker image includes Sharp dependencies
- [ ] Adequate disk space for image cache
- [ ] Memory limits account for Sharp processing
---
Accessibility
Alt Text
- [ ] ALL images have
altattribute - [ ] Meaningful alt for informative images
- [ ] Empty
alt=""for decorative images - [ ] Alt text describes content, not appearance
- [ ] No "image of" or "picture of" prefix
// ✅ GOOD: Meaningful alt
<Image src="/product.jpg" alt="Red Nike Air Max 90 running shoe, side view" />
// ✅ GOOD: Decorative image
<Image src="/decorative-pattern.svg" alt="" />
// ❌ BAD: Generic alt
<Image src="/product.jpg" alt="Image" />
// ❌ BAD: Missing alt
<Image src="/product.jpg" />Additional A11y
- [ ] No text in images (use real text)
- [ ] Sufficient color contrast for overlaid text
- [ ] Images don't convey information unavailable in text
- [ ] Decorative images marked with
role="presentation"
---
Performance Monitoring
Metrics to Track
- [ ] LCP (Largest Contentful Paint) < 2.5s
- [ ] CLS (Cumulative Layout Shift) = 0 for images
- [ ] Image load times in RUM data
- [ ] Total image bytes transferred
Debugging
- [ ] Check DevTools Network tab for actual sizes
- [ ] Verify format negotiation (AVIF/WebP served)
- [ ] Test on slow connections (DevTools throttling)
- [ ] Run Lighthouse for image recommendations
---
Error Handling
Fallbacks
- [ ] Fallback image configured for load errors
- [ ] Graceful degradation for broken images
- [ ] Error boundaries for image-heavy components
const [error, setError] = useState(false);
<Image
src={error ? '/fallback.jpg' : product.image}
onError={() => setError(true)}
/>Monitoring
- [ ] Image errors logged to monitoring service
- [ ] Alerts for high error rates
- [ ] 404s for images tracked
---
Build Pipeline
Optimization
- [ ] Images optimized at build time (where possible)
- [ ] Source images stored at high resolution
- [ ] Build includes image processing (Sharp, Squoosh)
- [ ] CI validates image configurations
Version Control
- [ ] Large images in Git LFS (not regular Git)
- [ ] Or: Images stored externally (CMS, CDN)
- [ ] Build pulls images from source
---
Security
Content Security
- [ ] Only allow trusted image domains
- [ ] SVG sanitization if user-uploaded
- [ ]
dangerouslyAllowSVG: falsein production - [ ] Rate limiting on image optimization endpoints
Privacy
- [ ] Strip EXIF metadata from user uploads
- [ ] No personally identifiable information in image URLs
- [ ] Consider image hashing for user content
Inference Optimization Checklist
Performance validation for LLM inference.
vLLM Configuration
- [ ] Tensor parallelism configured for GPU count
- [ ] Max model length set appropriately
- [ ] GPU memory utilization optimized (0.85-0.95)
- [ ] Prefix caching enabled for shared contexts
- [ ] Continuous batching active
Quantization
- [ ] Quantization method selected:
- FP16: Maximum quality, baseline
- INT8/FP8: Balance quality/efficiency
- AWQ: Best 4-bit quality
- GPTQ: Faster quantization
- [ ] Calibration data used (for GPTQ)
- [ ] Quality validated post-quantization
Speculative Decoding
- [ ] Method selected:
- N-gram: No extra model, lower overhead
- Draft model: Higher quality speculation
- [ ] Speculative tokens tuned (3-5 typical)
- [ ] Throughput improvement validated
Hardware Utilization
- [ ] GPU memory fully utilized
- [ ] Multi-GPU scaling verified
- [ ] NVLink/PCIe bandwidth sufficient
- [ ] CPU not bottlenecking
Batching Strategy
- [ ] Continuous batching enabled
- [ ] Max batch size configured
- [ ] Request prioritization (if needed)
- [ ] Queue management configured
Caching
- [ ] KV cache optimized (PagedAttention)
- [ ] Prefix caching for shared prompts
- [ ] Response caching (semantic if applicable)
- [ ] Cache invalidation strategy
Benchmarking
- [ ] Baseline latency measured
- [ ] Throughput (tokens/sec) benchmarked
- [ ] Time to first token (TTFT) measured
- [ ] Latency under load tested
- [ ] Memory usage profiled
Production Readiness
- [ ] Warmup requests sent before traffic
- [ ] Health checks configured
- [ ] Graceful shutdown handling
- [ ] Request timeout configured
- [ ] Error recovery tested
Monitoring
- [ ] Latency metrics (p50, p95, p99)
- [ ] Throughput tracking
- [ ] GPU utilization monitoring
- [ ] Memory usage tracking
- [ ] Error rate alerting
Cost Optimization
- [ ] Instance size appropriate
- [ ] Spot instances (if applicable)
- [ ] Auto-scaling configured
- [ ] Usage patterns analyzed
- [ ] Cost per request tracked
Performance Audit Checklist
Comprehensive guide for identifying and fixing performance bottlenecks, based on OrchestKit's real optimization process.
Prerequisites
- [ ] Access to production metrics (Prometheus, Grafana)
- [ ] Profiling tools installed (py-spy, Chrome DevTools)
- [ ] Baseline performance metrics captured
- [ ] Test environment with production-like data
Phase 1: Establish Baselines
Backend Metrics
Capture current performance:
# Database query performance
psql -c "SELECT query, calls, mean_time, total_time
FROM pg_stat_statements
ORDER BY total_time DESC LIMIT 20;"
# API latency
curl 'http://localhost:9090/api/v1/query?query=histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))'
# Cache hit rate
curl 'http://localhost:9090/api/v1/query?query=sum(rate(cache_operations_total{result="hit"}[5m])) / sum(rate(cache_operations_total[5m]))'- [ ] Record p50/p95/p99 latency for all endpoints
- [ ] Document slow queries (>100ms)
- [ ] Measure cache hit rates
- [ ] Capture database connection pool usage
- [ ] Record LLM token usage and costs
Frontend Metrics
Run Lighthouse audit:
# Lighthouse CLI
lighthouse http://localhost:3000 \
--output json \
--output-path lighthouse-report.json
# Or use Chrome DevTools → Lighthouse tab- [ ] Record Core Web Vitals (LCP, INP, CLS, TTFB)
- [ ] Measure bundle size (JS, CSS)
- [ ] Check for render-blocking resources
- [ ] Analyze long tasks (>50ms)
- [ ] Measure First Contentful Paint (FCP)
Baseline Targets
| Metric | Good | Needs Work | Current |
|---|---|---|---|
| p95 API latency | <500ms | <1s | ___ms |
| p95 DB query | <100ms | <500ms | ___ms |
| Cache hit rate | >70% | >50% | __% |
| LCP | <2.5s | <4s | ___s |
| INP | <200ms | <500ms | ___ms |
| CLS | <0.1 | <0.25 | ___ |
| Bundle size | <300KB | <500KB | ___KB |
Phase 2: Identify Bottlenecks
Backend Profiling
1. Find Slow Endpoints
# Top 10 slowest endpoints (p95 latency)
topk(10,
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
) by (endpoint)
)- [ ] List endpoints with p95 > 500ms
- [ ] Prioritize by traffic volume (high traffic = high impact)
- [ ] Document expected vs actual latency
2. Identify Slow Database Queries
-- Top 10 slowest queries
SELECT
LEFT(query, 80) as query_preview,
calls,
ROUND(mean_exec_time::numeric, 2) as avg_ms,
ROUND(total_exec_time::numeric, 2) as total_ms,
ROUND(100.0 * shared_blks_hit / NULLIF(shared_blks_hit + shared_blks_read, 0), 2) as cache_hit_ratio
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;- [ ] Run EXPLAIN ANALYZE on slow queries
- [ ] Check for sequential scans (should use indexes)
- [ ] Look for low cache hit ratios (<90%)
- [ ] Identify N+1 query patterns
3. Python Profiling with py-spy
# Profile running FastAPI server
py-spy record --pid $(pgrep -f uvicorn) \
--output profile.svg \
--duration 60
# Top functions by time
py-spy top --pid $(pgrep -f uvicorn)- [ ] Generate flame graph
- [ ] Identify hot paths (wide bars = time spent)
- [ ] Look for unexpected CPU usage
- [ ] Check for blocking I/O in async code
4. LLM Cost Analysis
-- Cost breakdown by model (Langfuse)
SELECT
model,
COUNT(*) as calls,
SUM(input_tokens) as total_input,
SUM(output_tokens) as total_output,
SUM(calculated_total_cost) as total_cost
FROM langfuse.traces
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY model
ORDER BY total_cost DESC;- [ ] Identify most expensive models
- [ ] Calculate cache hit rate potential
- [ ] Find repetitive queries (caching candidates)
- [ ] Measure prompt token waste
Frontend Profiling
1. Chrome DevTools Performance Tab
- [ ] Record 6s of user interaction
- [ ] Identify long tasks (yellow bars >50ms)
- [ ] Check for dropped frames (should be 60fps)
- [ ] Measure main thread blocking time
2. React DevTools Profiler
// Add Profiler to key components
import { Profiler } from 'react';
function onRenderCallback(
id, phase, actualDuration, baseDuration
) {
if (actualDuration > 16) {
console.warn(`Slow render: ${id} took ${actualDuration}ms`);
}
}
<Profiler id="AnalysisCard" onRender={onRenderCallback}>
<AnalysisCard />
</Profiler>- [ ] Find components with >16ms render time
- [ ] Identify unnecessary re-renders
- [ ] Check for missing memoization
3. Bundle Analysis
# Vite
npm run build
npx vite-bundle-visualizer
# Next.js
ANALYZE=true npm run build- [ ] Identify largest chunks
- [ ] Find duplicate dependencies
- [ ] Check for tree-shaking failures
- [ ] Measure code splitting effectiveness
Phase 3: Database Optimization
Add Missing Indexes
1. Identify Missing Indexes
-- Find sequential scans that should use indexes
SELECT
schemaname,
tablename,
seq_scan,
idx_scan,
seq_scan - idx_scan as too_much_seq
FROM pg_stat_user_tables
WHERE seq_scan - idx_scan > 0
ORDER BY too_much_seq DESC
LIMIT 10;- [ ] Run EXPLAIN ANALYZE on slow queries
- [ ] Look for "Seq Scan" in query plans
- [ ] Identify columns in WHERE/JOIN clauses
- [ ] Create indexes for high-cardinality columns
2. Create Indexes
-- B-tree for exact matches and ranges
CREATE INDEX idx_analysis_status ON analyses(status);
CREATE INDEX idx_analysis_created ON analyses(created_at DESC);
-- GIN for full-text search
CREATE INDEX idx_chunk_tsvector ON chunks USING GIN(content_tsvector);
-- HNSW for vector similarity (pgvector)
CREATE INDEX idx_chunk_embedding ON chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Composite index for common filter combinations
CREATE INDEX idx_chunk_analysis_created ON chunks(analysis_id, created_at DESC);- [ ] Create indexes for WHERE clause columns
- [ ] Use composite indexes for multi-column filters
- [ ] Add indexes for JOIN columns
- [ ] Use CONCURRENTLY for production
- [ ] Verify indexes are used (EXPLAIN ANALYZE)
Index Selection Guide:
| Query Pattern | Index Type | Example |
|---|---|---|
| Exact match | B-tree | WHERE status = 'completed' |
| Range query | B-tree | WHERE created_at > '2025-01-01' |
| Full-text search | GIN | WHERE content_tsvector @@ query |
| Vector similarity | HNSW | ORDER BY embedding <=> query_vec |
| JSONB queries | GIN | WHERE metadata @> '{"key": "value"}' |
Fix N+1 Queries
1. Detect N+1 Patterns
# ❌ BAD: N+1 query (1 query + N queries in loop)
analyses = await session.execute(select(Analysis).limit(10))
for analysis in analyses.scalars():
# Each iteration = 1 query!
chunks = await session.execute(
select(Chunk).where(Chunk.analysis_id == analysis.id)
)- [ ] Review logs for rapid sequential queries
- [ ] Check for queries inside loops
- [ ] Use query count logging in tests
2. Fix with Eager Loading
# ✅ GOOD: Single query with JOIN
from sqlalchemy.orm import selectinload
analyses = await session.execute(
select(Analysis)
.options(selectinload(Analysis.chunks)) # Eager load
.limit(10)
).scalars().all()
# Now analyses[0].chunks is preloaded (no extra query)- [ ] Replace lazy loading with eager loading
- [ ] Use
selectinload()for one-to-many - [ ] Use
joinedload()for one-to-one - [ ] Verify query count reduced (N+1 → 1-2 queries)
Optimize Connection Pooling
1. Check Current Pool Usage
# Connection pool saturation
db_connections_active / db_connections_max- [ ] Measure active vs max connections
- [ ] Check for pool exhaustion (ratio >0.8)
- [ ] Monitor connection wait times
2. Configure Pool
# backend/app/core/config.py
from sqlalchemy import create_engine
engine = create_engine(
database_url,
pool_size=5, # Connections to maintain
max_overflow=10, # Extra connections allowed
pool_recycle=3600, # Recycle after 1 hour
pool_pre_ping=True # Validate before checkout
)- [ ] Set pool_size based on traffic (5-20 typical)
- [ ] Allow overflow for spikes
- [ ] Enable pool_pre_ping for stale detection
- [ ] Set pool_recycle to avoid timeouts
Phase 4: Caching Strategy
Identify Caching Opportunities
1. Find Repetitive Queries
-- Most frequently called queries
SELECT
LEFT(query, 80),
calls,
ROUND(mean_exec_time::numeric, 2) as avg_ms
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 20;- [ ] Identify high-frequency queries
- [ ] Check if data changes frequently
- [ ] Calculate potential savings (calls × avg_time)
2. Find Repetitive LLM Calls
-- Similar prompts (Langfuse)
SELECT
LEFT(input::text, 100) as prompt_preview,
COUNT(*) as occurrences,
SUM(calculated_total_cost) as total_cost
FROM langfuse.generations
GROUP BY LEFT(input::text, 100)
HAVING COUNT(*) > 5
ORDER BY total_cost DESC;- [ ] Identify repetitive prompts
- [ ] Calculate cost savings potential
- [ ] Determine appropriate cache TTL
Implement Multi-Level Cache
L1: In-Memory Cache (Application)
from functools import lru_cache
@lru_cache(maxsize=128)
def get_agent_system_prompt(agent_type: str) -> str:
"""Cache agent prompts in memory."""
return load_prompt_from_file(f"prompts/{agent_type}.txt")- [ ] Cache static data (prompts, configs)
- [ ] Use LRU cache for bounded memory
- [ ] Set appropriate maxsize (128-1024)
L2: Redis Cache (Distributed)
async def get_analysis(analysis_id: str) -> Analysis:
"""Cache analysis results in Redis."""
# Try cache first
cached = await redis.get(f"analysis:{analysis_id}")
if cached:
return Analysis.parse_raw(cached)
# Cache miss - fetch from DB
analysis = await db.get_analysis(analysis_id)
# Store in cache (5 min TTL)
await redis.setex(
f"analysis:{analysis_id}",
300,
analysis.json()
)
return analysis- [ ] Cache query results
- [ ] Set appropriate TTL (seconds to hours)
- [ ] Invalidate on writes
- [ ] Track cache hit rate
L3: Semantic Cache (Vector Search)
async def get_llm_response(query: str) -> str:
"""Check semantic cache before calling LLM."""
# Generate query embedding
embedding = await embed_text(query)
# Search for similar cached queries
cached = await semantic_cache.search(embedding, threshold=0.92)
if cached:
return cached.response
# Call LLM
response = await llm.complete(query)
# Store in cache
await semantic_cache.store(embedding, response)
return response- [ ] Cache LLM responses by semantic similarity
- [ ] Set similarity threshold (0.90-0.95)
- [ ] Measure cost savings
- [ ] Monitor false positive rate
Cache Invalidation
Write-Through Pattern:
async def update_analysis(analysis: Analysis):
"""Update DB and cache atomically."""
# 1. Write to DB
await db.update(analysis)
# 2. Update cache
await redis.setex(
f"analysis:{analysis.id}",
300,
analysis.json()
)- [ ] Invalidate cache on writes
- [ ] Use TTL for time-sensitive data
- [ ] Add cache versioning for schema changes
Phase 5: Frontend Optimization
Code Splitting
1. Route-Based Splitting
// Before: All routes in one bundle
import AnalysisPage from './pages/AnalysisPage';
import DashboardPage from './pages/DashboardPage';
// After: Lazy load routes
const AnalysisPage = lazy(() => import('./pages/AnalysisPage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/analysis" element={<AnalysisPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</Suspense>- [ ] Lazy load routes
- [ ] Add loading states
- [ ] Measure bundle size reduction
2. Component-Level Splitting
// Lazy load heavy components
const ChartComponent = lazy(() => import('./ChartComponent'));
{showChart && (
<Suspense fallback={<Skeleton />}>
<ChartComponent data={data} />
</Suspense>
)}- [ ] Split large dependencies (charts, editors)
- [ ] Use dynamic imports for modals
- [ ] Prefetch on user intent (hover, focus)
Memoization
React.memo for Components:
// Prevent re-renders when props unchanged
const AnalysisCard = memo(({ analysis }: Props) => {
return <div>{analysis.title}</div>;
});- [ ] Wrap expensive components with memo()
- [ ] Verify props don't change unnecessarily
- [ ] Use React DevTools Profiler to confirm
useMemo for Expensive Calculations:
const expensiveValue = useMemo(() => {
return processLargeDataset(data);
}, [data]); // Only recompute if data changes- [ ] Memoize expensive calculations
- [ ] Memoize filtered/sorted arrays
- [ ] Don't over-memoize (profiling first!)
useCallback for Event Handlers:
const handleClick = useCallback(() => {
doSomething(id);
}, [id]); // Only recreate if id changes
<ChildComponent onClick={handleClick} />- [ ] Wrap callbacks passed to memoized children
- [ ] Avoid inline functions in props
- [ ] Include all dependencies
Image Optimization
// Use next/image or similar for optimization
<Image
src="/photo.jpg"
alt="Description"
width={800}
height={600}
loading="lazy" // Lazy load images
placeholder="blur" // Show blur while loading
/>- [ ] Use WebP/AVIF formats
- [ ] Lazy load images below the fold
- [ ] Set explicit width/height (prevent CLS)
- [ ] Use responsive images (srcset)
Phase 6: Measure Impact
Re-Run Benchmarks
Backend:
# Query performance
psql -c "SELECT query, mean_time FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;"
# API latency
curl 'http://localhost:9090/api/v1/query?query=histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))'Frontend:
lighthouse http://localhost:3000 --output json- [ ] Compare p95 latency (before vs after)
- [ ] Verify query performance improved
- [ ] Check cache hit rates increased
- [ ] Measure Core Web Vitals improvement
Calculate Savings
Cost Savings:
# LLM cost reduction
baseline_cost = 35000 # Annual
cache_hit_rate = 0.90
savings = baseline_cost * cache_hit_rate * 0.90 # 90% discount on cache hits
final_cost = baseline_cost - savingsPerformance Gains:
# Query speedup
before_latency = 85 # ms
after_latency = 5 # ms
speedup = before_latency / after_latency # 17x- [ ] Document cost savings
- [ ] Calculate ROI (savings vs implementation time)
- [ ] Measure user experience improvement
Create Performance Budget
Set ongoing targets:
- [ ] p95 API latency < 500ms
- [ ] p95 DB query < 100ms
- [ ] Cache hit rate > 70%
- [ ] LCP < 2.5s
- [ ] Bundle size < 300KB
Monitor continuously:
- [ ] Add Lighthouse CI to pipeline
- [ ] Alert on budget violations
- [ ] Review metrics weekly
Phase 7: Ongoing Optimization
Weekly Reviews
- [ ] Review top 10 slowest endpoints
- [ ] Check for new slow queries
- [ ] Monitor cache hit rates
- [ ] Review LLM cost trends
- [ ] Check Core Web Vitals in RUM
Monthly Audits
- [ ] Run full Lighthouse audit
- [ ] Profile with py-spy/Chrome DevTools
- [ ] Review database index usage
- [ ] Check for unused dependencies
- [ ] Update performance budget
Continuous Monitoring
- [ ] Set up alerts for degradation
- [ ] Track performance in CI/CD
- [ ] Monitor real user metrics (RUM)
- [ ] A/B test optimizations
References
- Example:
../examples/orchestkit-performance-wins.md - Template:
../scripts/caching-patterns.ts - Template:
../scripts/database-optimization.ts - Chrome DevTools Performance
- Lighthouse Documentation
- PostgreSQL EXPLAIN
React Performance Audit Checklist
Pre-deployment performance verification.
React Compiler Check
- [ ] React Compiler enabled in build config
- [ ] Components show "Memo ✨" badge in DevTools
- [ ] Code follows Rules of React:
- [ ] Components are idempotent
- [ ] Props/state treated as immutable
- [ ] Side effects in useEffect only
- [ ] Hooks at top level
Render Performance
- [ ] No unnecessary re-renders (verified with Profiler)
- [ ] State colocated close to usage
- [ ] Context split to prevent cascading updates
- [ ] Expensive computations have escape hatch memoization
- [ ] Lists > 100 items are virtualized
Large Lists / Data
- [ ] TanStack Virtual for lists > 100 items
- [ ] Pagination or infinite scroll for API data
- [ ] Table virtualization for grids > 50 rows
- [ ] Images lazy loaded below fold
Code Splitting
- [ ] Route-based code splitting (lazy routes)
- [ ] Heavy components lazy loaded
- [ ] Dynamic imports for large libraries
- [ ] Bundle analyzer run, no unexpected large chunks
Network Performance
- [ ] API calls deduplicated (React Query, SWR)
- [ ] Data prefetched on hover/intent
- [ ] Optimistic updates for mutations
- [ ] Appropriate cache headers set
Images & Media
- [ ] Images optimized (WebP, AVIF)
- [ ] Responsive images with srcset
- [ ] Lazy loading for below-fold images
- [ ] Placeholder/skeleton during load
Third-Party Scripts
- [ ] Analytics loaded async/deferred
- [ ] Third-party widgets lazy loaded
- [ ] Font loading optimized (preload critical)
- [ ] No render-blocking resources
Profiling Verification
Before Optimization
1. [ ] Record baseline interaction times 2. [ ] Document slowest components 3. [ ] Note current bundle size
After Optimization
1. [ ] Re-profile all interactions 2. [ ] Verify improvements in numbers 3. [ ] Check bundle size delta
Key Metrics to Track
| Metric | Target | Current |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | ___ |
| FID (First Input Delay) | < 100ms | ___ |
| CLS (Cumulative Layout Shift) | < 0.1 | ___ |
| Time to Interactive | < 3s | ___ |
| Main thread blocking | < 200ms | ___ |
Quick Profiler Commands
# React DevTools Profiler
# 1. Open DevTools → Profiler tab
# 2. Click Record
# 3. Perform interaction
# 4. Click Stop
# 5. Analyze flamegraph
# Lighthouse
npx lighthouse http://localhost:3000 --view
# Bundle Analyzer (Next.js)
ANALYZE=true npm run build
# Bundle Analyzer (Vite)
npx vite-bundle-visualizerCommon Issues Checklist
- [ ] No anonymous functions as props in hot paths
- [ ] No object/array literals as props in hot paths
- [ ] Context providers near consumers
- [ ] useEffect dependencies correct
- [ ] No state updates in render
Sign-Off
- [ ] All critical interactions < 100ms
- [ ] No visible jank during scroll
- [ ] Page load acceptable on 3G
- [ ] Bundle size within budget
- [ ] Performance regression tests in CI
Core Web Vitals Examples
Real-world optimization examples for LCP, INP, and CLS.
---
1. LCP Optimization: E-Commerce Hero Section
Complete optimization of a hero section with product image and CTA.
Before: Slow LCP (3.5s+)
// ❌ BAD: Multiple LCP issues
function Hero() {
const [product, setProduct] = useState(null);
useEffect(() => {
// Problem 1: LCP content loaded client-side
fetch('/api/featured-product')
.then(res => res.json())
.then(setProduct);
}, []);
if (!product) return <div className="h-[600px]" />; // Problem 2: No skeleton
return (
<div className="relative">
{/* Problem 3: No priority, lazy by default */}
<img src={product.image} alt={product.name} />
<h1>{product.name}</h1>
<a href={`/product/${product.id}`}>Shop Now</a>
</div>
);
}After: Optimized LCP (1.2s)
// ✅ GOOD: Server-rendered with optimized image
import Image from 'next/image';
import { Suspense } from 'react';
// Server Component - data fetched on server
async function Hero() {
// Fetched on server, included in initial HTML
const product = await getFeaturedProduct();
return (
<section className="relative h-[600px] overflow-hidden">
{/* Priority image with explicit dimensions */}
<Image
src={product.image}
alt={product.name}
fill
priority // Preloads, eager loading
sizes="100vw"
quality={85}
placeholder="blur"
blurDataURL={product.blurPlaceholder}
style={{ objectFit: 'cover' }}
/>
{/* Content overlay */}
<div className="relative z-10 flex flex-col items-center justify-center h-full text-white">
<h1 className="text-5xl font-bold">{product.name}</h1>
<p className="mt-4 text-xl">{product.tagline}</p>
<a
href={`/product/${product.id}`}
className="mt-8 px-8 py-4 bg-white text-black rounded-lg font-semibold"
>
Shop Now
</a>
</div>
</section>
);
}
// Loading skeleton for Suspense boundary
function HeroSkeleton() {
return (
<section className="relative h-[600px] bg-gray-200 animate-pulse">
<div className="flex flex-col items-center justify-center h-full">
<div className="h-12 w-64 bg-gray-300 rounded" />
<div className="mt-4 h-6 w-48 bg-gray-300 rounded" />
<div className="mt-8 h-14 w-40 bg-gray-300 rounded-lg" />
</div>
</section>
);
}
// Usage in page
export default function HomePage() {
return (
<Suspense fallback={<HeroSkeleton />}>
<Hero />
</Suspense>
);
}
// Also add preload in head (layout.tsx or page metadata)
export const metadata = {
other: {
'link': [
{
rel: 'preload',
as: 'image',
href: '/featured-product-hero.webp',
fetchpriority: 'high',
},
],
},
};Document Head Optimizations
<!-- Add to <head> for fastest LCP -->
<head>
<!-- Preload hero image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- Preload critical font -->
<link
rel="preload"
as="font"
href="/fonts/inter-bold.woff2"
type="font/woff2"
crossorigin
/>
<!-- Preconnect to image CDN -->
<link rel="preconnect" href="https://images.example.com" />
<!-- DNS prefetch for analytics -->
<link rel="dns-prefetch" href="https://analytics.example.com" />
</head>---
2. INP Optimization: Product Search Filter
Optimizing a search filter that was causing 400ms+ INP.
Before: Blocking INP (400ms+)
// ❌ BAD: Blocks main thread on every keystroke
function ProductSearch({ products }: { products: Product[] }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState(products);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
// Problem: Expensive filter runs synchronously
// Blocks paint until complete
const filtered = products.filter(p =>
p.name.toLowerCase().includes(value.toLowerCase()) ||
p.description.toLowerCase().includes(value.toLowerCase()) ||
p.tags.some(t => t.toLowerCase().includes(value.toLowerCase()))
);
setResults(filtered);
};
return (
<>
<input
value={query}
onChange={handleChange}
placeholder="Search products..."
/>
<ProductGrid products={results} />
</>
);
}After: Responsive INP (50ms)
// ✅ GOOD: Non-blocking with useDeferredValue
import {
useState,
useDeferredValue,
useMemo,
useTransition,
memo
} from 'react';
function ProductSearch({ products }: { products: Product[] }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
// Deferred value for expensive computation
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// Memoized filter only runs when deferredQuery changes
const results = useMemo(() => {
if (!deferredQuery) return products;
const searchLower = deferredQuery.toLowerCase();
return products.filter(p =>
p.name.toLowerCase().includes(searchLower) ||
p.description.toLowerCase().includes(searchLower) ||
p.tags.some(t => t.toLowerCase().includes(searchLower))
);
}, [products, deferredQuery]);
return (
<div>
<div className="relative">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
className="w-full px-4 py-2 border rounded-lg"
/>
{/* Loading indicator during filter */}
{isPending && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Spinner size="sm" />
</div>
)}
</div>
{/* Fade during pending state */}
<div
className="mt-4 transition-opacity"
style={{ opacity: isStale ? 0.7 : 1 }}
>
<ProductGrid products={results} />
</div>
</div>
);
}
// Memoized grid to prevent unnecessary re-renders
const ProductGrid = memo(function ProductGrid({
products
}: {
products: Product[]
}) {
return (
<div className="grid grid-cols-4 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
});For Very Large Lists: Virtual Scrolling
// ✅ BEST: Virtualization for huge lists
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedProductList({ products }: { products: Product[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: products.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 200, // Estimated row height
overscan: 5, // Render 5 extra items above/below
});
return (
<div
ref={parentRef}
className="h-[600px] overflow-auto"
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<ProductCard product={products[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}---
3. CLS Optimization: News Article Page
Fixing layout shifts from images, ads, and fonts.
Before: High CLS (0.35)
// ❌ BAD: Multiple CLS issues
function Article({ article }: { article: Article }) {
const [ad, setAd] = useState(null);
useEffect(() => {
loadAd().then(setAd);
}, []);
return (
<article>
<h1>{article.title}</h1>
{/* Problem 1: Image without dimensions */}
<img src={article.heroImage} alt="" />
{/* Problem 2: Ad appears after load, shifts content */}
{ad && <div className="ad-banner"><img src={ad.image} /></div>}
<div dangerouslySetInnerHTML={{ __html: article.content }} />
{/* Problem 3: Related articles load and shift */}
<RelatedArticles />
</article>
);
}
// Problem 4: Font causes layout shift
// CSS
/* No font-display, no fallback sizing */
@font-face {
font-family: 'CustomFont';
src: url('/font.woff2');
}After: Zero CLS (0.0)
// ✅ GOOD: All layout shifts prevented
import Image from 'next/image';
function Article({ article }: { article: Article }) {
return (
<article className="max-w-3xl mx-auto">
<h1 className="text-4xl font-bold">{article.title}</h1>
{/* Fixed dimensions prevent shift */}
<div className="relative aspect-[16/9] my-6">
<Image
src={article.heroImage}
alt={article.heroAlt}
fill
sizes="(max-width: 768px) 100vw, 768px"
priority
style={{ objectFit: 'cover' }}
/>
</div>
{/* Reserved space for ad */}
<AdSlot
slot="article-top"
className="my-6"
minHeight={250}
/>
<div
className="prose prose-lg"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
{/* Reserved space for related */}
<RelatedArticles articleId={article.id} />
</article>
);
}
// Ad component with reserved space
function AdSlot({
slot,
className,
minHeight
}: {
slot: string;
className?: string;
minHeight: number;
}) {
const [ad, setAd] = useState<Ad | null>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
loadAd(slot).then(ad => {
setAd(ad);
setLoaded(true);
});
}, [slot]);
return (
<div
className={className}
style={{ minHeight: `${minHeight}px` }} // Reserved space
>
{loaded ? (
ad ? (
<Image
src={ad.image}
alt={ad.alt}
width={ad.width}
height={ad.height}
/>
) : null // No ad, space collapses gracefully
) : (
<Skeleton height={minHeight} /> // Placeholder during load
)}
</div>
);
}
// Related articles with skeleton
function RelatedArticles({ articleId }: { articleId: string }) {
const [articles, setArticles] = useState<Article[] | null>(null);
useEffect(() => {
fetchRelated(articleId).then(setArticles);
}, [articleId]);
return (
<section className="mt-12">
<h2 className="text-2xl font-bold mb-6">Related Articles</h2>
{/* Fixed grid prevents shift */}
<div className="grid grid-cols-3 gap-6">
{articles ? (
articles.map(article => (
<ArticleCard key={article.id} article={article} />
))
) : (
// Skeleton matches final layout exactly
<>
<ArticleCardSkeleton />
<ArticleCardSkeleton />
<ArticleCardSkeleton />
</>
)}
</div>
</section>
);
}
// Skeleton that matches card dimensions exactly
function ArticleCardSkeleton() {
return (
<div className="animate-pulse">
<div className="aspect-[16/9] bg-gray-200 rounded-lg" />
<div className="mt-3 h-5 bg-gray-200 rounded w-3/4" />
<div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />
</div>
);
}Font Loading Without CLS
/* ✅ Optimized font loading */
/* Main font with swap and metrics */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
font-weight: 100 900;
}
/* Fallback font with matched metrics */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.64%;
ascent-override: 90%;
descent-override: 22.43%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
}
/* Alternative: font-display: optional for non-critical fonts */
@font-face {
font-family: 'DisplayFont';
src: url('/fonts/display.woff2') format('woff2');
font-display: optional; /* Won't cause FOUT - uses fallback if not cached */
}---
4. Complete RUM Implementation
Full Real User Monitoring setup with Next.js.
// lib/performance.ts
import { onCLS, onINP, onLCP, onFCP, onTTFB, type Metric } from 'web-vitals';
const ENDPOINT = '/api/vitals';
interface EnrichedMetric {
name: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
delta: number;
id: string;
navigationType: string;
url: string;
timestamp: number;
connection?: string;
deviceMemory?: number;
viewport: { width: number; height: number };
}
function getConnectionInfo() {
const nav = navigator as Navigator & {
connection?: { effectiveType?: string };
deviceMemory?: number;
};
return {
connection: nav.connection?.effectiveType,
deviceMemory: nav.deviceMemory,
};
}
function sendMetric(metric: Metric) {
const enriched: EnrichedMetric = {
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
url: window.location.href,
timestamp: Date.now(),
...getConnectionInfo(),
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
};
// Use sendBeacon for reliability
if (navigator.sendBeacon) {
navigator.sendBeacon(ENDPOINT, JSON.stringify(enriched));
} else {
fetch(ENDPOINT, {
method: 'POST',
body: JSON.stringify(enriched),
keepalive: true,
});
}
// Debug in development
if (process.env.NODE_ENV === 'development') {
const color = {
good: 'green',
'needs-improvement': 'orange',
poor: 'red',
}[metric.rating];
console.log(
`%c[${metric.name}] ${metric.value.toFixed(1)}${metric.name === 'CLS' ? '' : 'ms'}`,
`color: ${color}; font-weight: bold`
);
}
}
export function initWebVitals() {
onCLS(sendMetric);
onINP(sendMetric);
onLCP(sendMetric);
onFCP(sendMetric);
onTTFB(sendMetric);
}// app/components/web-vitals.tsx
'use client';
import { useEffect } from 'react';
import { initWebVitals } from '@/lib/performance';
export function WebVitals() {
useEffect(() => {
initWebVitals();
}, []);
return null;
}// app/api/vitals/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface VitalMetric {
name: string;
value: number;
rating: string;
url: string;
timestamp: number;
}
export async function POST(request: NextRequest) {
const metric: VitalMetric = await request.json();
// Log for debugging
console.log('[Vital]', metric.name, metric.value, metric.rating);
// Store in database (example with Drizzle)
// await db.insert(webVitals).values({
// name: metric.name,
// value: metric.value,
// rating: metric.rating,
// url: metric.url,
// timestamp: new Date(metric.timestamp),
// });
// Alert on poor metrics
if (metric.rating === 'poor') {
// await alertService.send({
// severity: 'warning',
// message: `Poor ${metric.name}: ${metric.value} on ${metric.url}`,
// });
}
return NextResponse.json({ ok: true });
}---
5. Performance Budget Enforcement
CI/CD integration with Lighthouse CI.
lighthouserc.js
module.exports = {
ci: {
collect: {
url: [
'http://localhost:3000/',
'http://localhost:3000/products',
'http://localhost:3000/checkout',
],
numberOfRuns: 3,
settings: {
preset: 'desktop',
// Throttle to simulate 4G
// throttling: { ... }
},
},
assert: {
assertions: {
// Core Web Vitals
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 200 }], // Proxy for INP
// Other performance metrics
'first-contentful-paint': ['warn', { maxNumericValue: 1800 }],
'speed-index': ['warn', { maxNumericValue: 3400 }],
// Resource budgets
'resource-summary:script:size': ['error', { maxNumericValue: 150000 }],
'resource-summary:image:size': ['error', { maxNumericValue: 300000 }],
'resource-summary:total:size': ['error', { maxNumericValue: 500000 }],
// Scores
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};GitHub Actions Workflow
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Start server
run: npm start &
- name: Wait for server
run: npx wait-on http://localhost:3000
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: lighthouse-results
path: .lighthouseci/---
Quick Reference
// ✅ LCP: Server-render, preload, priority
export default async function Page() {
const data = await getData(); // Server-side
return <Image src={data.hero} priority fill />;
}
// ✅ INP: useTransition for expensive updates
const [isPending, startTransition] = useTransition();
onChange={(e) => {
setQuery(e.target.value);
startTransition(() => setResults(filter(e.target.value)));
}}
// ✅ CLS: Always set dimensions
<Image src="/photo.jpg" width={800} height={600} />
<div className="aspect-[16/9]"><Image fill /></div>
<div className="min-h-[250px]">{content}</div>
// ✅ RUM: Send metrics reliably
navigator.sendBeacon('/api/vitals', JSON.stringify(metric));
// ✅ Debug: Find LCP element
new PerformanceObserver((list) => {
console.log('LCP:', list.getEntries().at(-1)?.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });Image Optimization Examples
Hero Image with Blur Placeholder
import Image from 'next/image';
import heroImage from '@/public/hero.jpg'; // Static import
function Hero() {
return (
<div className="relative h-[600px] w-full">
<Image
src={heroImage}
alt="Beautiful landscape"
fill
priority
placeholder="blur" // Automatic with static import
sizes="100vw"
style={{ objectFit: 'cover' }}
/>
<div className="absolute inset-0 flex items-center justify-center">
<h1 className="text-5xl font-bold text-white">Welcome</h1>
</div>
</div>
);
}Product Grid with Responsive Sizes
function ProductGrid({ products }) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{products.map((product) => (
<div key={product.id} className="relative aspect-square">
<Image
src={product.imageUrl}
alt={product.name}
fill
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
className="object-cover rounded-lg"
/>
</div>
))}
</div>
);
}Avatar with Fallback
function UserAvatar({ user }) {
const [error, setError] = useState(false);
if (error || !user.avatarUrl) {
return (
<div className="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center">
<span className="text-white font-medium">
{user.name.charAt(0).toUpperCase()}
</span>
</div>
);
}
return (
<Image
src={user.avatarUrl}
alt={user.name}
width={40}
height={40}
className="rounded-full"
onError={() => setError(true)}
/>
);
}Art Direction (Different Crops)
function ResponsiveBanner() {
return (
<>
{/* Mobile: Portrait crop */}
<div className="relative h-[400px] md:hidden">
<Image
src="/banner-mobile.jpg"
alt="Banner"
fill
priority
sizes="100vw"
className="object-cover"
/>
</div>
{/* Desktop: Landscape crop */}
<div className="relative hidden h-[300px] md:block">
<Image
src="/banner-desktop.jpg"
alt="Banner"
fill
priority
sizes="100vw"
className="object-cover"
/>
</div>
</>
);
}Gallery with Lightbox
function ImageGallery({ images }) {
const [selected, setSelected] = useState(null);
return (
<>
<div className="grid grid-cols-3 gap-2">
{images.map((image, i) => (
<button
key={image.id}
onClick={() => setSelected(image)}
className="relative aspect-square"
>
<Image
src={image.thumbnailUrl}
alt={image.alt}
fill
sizes="33vw"
className="object-cover"
/>
</button>
))}
</div>
{selected && (
<Dialog open onClose={() => setSelected(null)}>
<div className="relative h-[80vh] w-[90vw]">
<Image
src={selected.fullUrl}
alt={selected.alt}
fill
sizes="90vw"
quality={90}
className="object-contain"
/>
</div>
</Dialog>
)}
</>
);
}Background Image Pattern
// For true background images, use CSS
function HeroWithCSSBackground() {
return (
<div
className="h-[600px] bg-cover bg-center"
style={{ backgroundImage: 'url(/hero.webp)' }}
>
<div className="h-full flex items-center justify-center bg-black/40">
<h1 className="text-white text-5xl">Hero Title</h1>
</div>
</div>
);
}
// For Next.js optimization, use Image with fill
function HeroWithNextImage() {
return (
<div className="relative h-[600px]">
<Image
src="/hero.webp"
alt=""
fill
priority
className="object-cover -z-10"
/>
<div className="h-full flex items-center justify-center bg-black/40">
<h1 className="text-white text-5xl">Hero Title</h1>
</div>
</div>
);
}OrchestKit Performance Wins - Real Optimization Examples
This document showcases actual performance optimizations from OrchestKit's production implementation with before/after metrics.
Overview
Key Performance Achievements:
- LLM costs: $35k/year → $2-5k/year (85-95% reduction)
- Vector search: 85ms → 5ms (17x faster)
- Retrieval accuracy: 87.2% → 91.6% (5.1% improvement)
- Quality gate pass rate: Increased from 67-77% → 85%+ (stable)
- Cache hit rate: 0% → 90% (L1) + 75% (L2)
Win 1: Multi-Level LLM Caching
Problem
Projected annual LLM costs: $35,000
- 8 agents per analysis, 1,500-1,800 tokens each
- Average 145 analyses/month
- No caching = every query hits LLM
- Claude Sonnet 4.5: $3/MTok input, $15/MTok output
Investigation
Cost breakdown by agent:
-- Langfuse query
SELECT
metadata->>'agent_type' as agent,
SUM(calculated_total_cost) as total_cost,
AVG(input_tokens) as avg_input,
AVG(output_tokens) as avg_output
FROM traces
GROUP BY agent
ORDER BY total_cost DESC;Results:
| Agent | Monthly Cost | Avg Input | Avg Output |
|---|---|---|---|
| security_auditor | $3.05 | 1,800 | 1,200 |
| implementation_planner | $2.76 | 1,600 | 1,100 |
| tech_comparator | $2.61 | 1,500 | 1,000 |
| Total (8 agents) | $18.73 | - | - |
Pain points:
- Analyzing similar content (React tutorials, FastAPI guides) repeatedly
- Security patterns (XSS, SQL injection) are common across codebases
- Implementation patterns (CRUD, auth) are highly repetitive
Solution: 3-Level Cache Hierarchy
Architecture:
Request → L1: Prompt Cache (Claude native)
↓ miss (10%)
→ L2: Semantic Cache (Redis vector search)
↓ miss (25% of L1 misses)
→ L3: LLM Call (actual cost)L1: Claude Prompt Caching (Native)
File: backend/app/shared/services/llm/anthropic_client.py
from anthropic import AsyncAnthropic
async def call_claude_with_prompt_cache(
system_prompt: str,
user_message: str,
model: str = "claude-sonnet-4-6"
) -> str:
"""Call Claude with prompt caching for system prompts."""
response = await anthropic_client.messages.create(
model=model,
max_tokens=4096,
system=[
{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"} # Cache this!
}
],
messages=[
{"role": "user", "content": user_message}
]
)
# Log cache usage
cache_hit = response.usage.cache_read_input_tokens > 0
logger.info("claude_prompt_cache",
cache_hit=cache_hit,
cache_read_tokens=response.usage.cache_read_input_tokens,
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens
)
return response.content[0].textCost savings:
- Cache hit: 90% discount on cached tokens
- Cache duration: 5 minutes
- Effective for: Agent system prompts (1,500+ tokens each)
L2: Semantic Cache (Redis + Vector Search)
File: backend/app/shared/services/cache/semantic_cache.py
from redis import Redis
from app.shared.services.embeddings import embed_text
import numpy as np
class SemanticCache:
"""Vector similarity-based cache for LLM responses."""
def __init__(self, redis_client: Redis, threshold: float = 0.92):
self.redis = redis_client
self.threshold = threshold # Cosine similarity threshold
async def get(self, query: str) -> str | None:
"""Check if semantically similar query exists in cache."""
# Generate query embedding
query_embedding = await embed_text(query)
# Search for similar cached queries
# (Using Redis VSS or dedicated vector store)
cached_queries = await self._vector_search(query_embedding, top_k=5)
for cached_query, cached_embedding, cached_response in cached_queries:
similarity = cosine_similarity(query_embedding, cached_embedding)
if similarity >= self.threshold:
logger.info("semantic_cache_hit",
similarity=similarity,
cached_query=cached_query[:100]
)
return cached_response
return None # Cache miss
async def set(self, query: str, response: str, ttl: int = 3600):
"""Store query-response pair with embedding."""
# Generate embedding
embedding = await embed_text(query)
# Store in Redis (with vector index)
cache_key = f"semantic_cache:{hash(query)}"
await self.redis.setex(
cache_key,
ttl,
json.dumps({
"query": query,
"response": response,
"embedding": embedding.tolist(),
"timestamp": datetime.now().isoformat()
})
)Cost savings:
- 75% hit rate on L1 misses
- Near-instant responses (5-10ms vs 2000ms)
- Effective for: Similar technical queries
Implementation in agent calls:
@observe(name="agent_execution")
async def execute_agent(agent_type: str, content: str) -> Finding:
"""Execute agent with 3-level caching."""
# Build query
system_prompt = get_agent_system_prompt(agent_type) # 1,500+ tokens
user_message = f"Analyze this content:\n\n{content[:8000]}"
# L2: Check semantic cache
cache_key = f"{agent_type}:{content[:200]}" # Simple key for demo
cached_response = await semantic_cache.get(cache_key)
if cached_response:
logger.info("cache_hit", level="L2_semantic", agent=agent_type)
return parse_finding(cached_response)
# L1 + L3: Call Claude (with prompt caching)
response = await call_claude_with_prompt_cache(
system_prompt=system_prompt, # Cached by Claude
user_message=user_message
)
# Store in semantic cache
await semantic_cache.set(cache_key, response, ttl=3600)
return parse_finding(response)Results
Cost Reduction:
Baseline (no cache): $35,000/year
L1 savings (90% hit): -$28,350 (90% discount on 90% of queries)
L2 savings (75% hit): -$4,650 (85% discount on 75% of L1 misses)
Final cost: $2,000-5,000/year
Total savings: 85-95%Latency Improvement:
| Cache Level | Hit Rate | Latency | Cost Savings |
|---|---|---|---|
| L1 (Prompt) | 90% | 2000ms (same) | 90% on cached tokens |
| L2 (Semantic) | 75% (of L1 misses) | 5-10ms | 85% (full skip) |
| L3 (LLM) | 2.5% (fallback) | 2000ms | 0% (full cost) |
Implementation effort: 2 days Maintenance overhead: Low (cache TTL auto-expires stale data)
Win 2: Vector Index Optimization (HNSW vs IVFFlat)
Problem
Vector search taking 85ms, needed <10ms
- Golden dataset: 415 chunks, 1536-dim embeddings
- IVFFlat index (lists=10)
- Hybrid search (vector + BM25 RRF) bottlenecked by vector search
Investigation
Benchmark both index types:
-- IVFFlat performance
EXPLAIN ANALYZE
SELECT * FROM chunks
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
-- Result:
-- Planning Time: 2.1 ms
-- Execution Time: 85.3 ms-- HNSW performance
CREATE INDEX idx_chunk_embedding_hnsw ON chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
EXPLAIN ANALYZE
SELECT * FROM chunks
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
-- Result:
-- Planning Time: 2.0 ms
-- Execution Time: 5.1 msTrade-offs:
| Index | Build Time | Query Time | Accuracy | Memory |
|---|---|---|---|---|
| IVFFlat (lists=10) | 2s | 85ms | 95% | Low |
| HNSW (m=16) | 8s | 5ms | 98% | Medium |
Solution: HNSW Index with Optimized Parameters
File: backend/alembic/versions/xxx_add_hnsw_index.py
def upgrade():
"""Add HNSW index for vector similarity search."""
op.execute("""
CREATE INDEX CONCURRENTLY idx_chunk_embedding_hnsw
ON chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
""")
# Drop old IVFFlat index
op.execute("DROP INDEX IF EXISTS idx_chunk_embedding_ivfflat;")Parameters chosen:
m = 16: Connections per layer (sweet spot for 1k-10k vectors)ef_construction = 64: Build-time quality (higher = better accuracy, slower build)ef_search = 64: Query-time quality (can tune per query)
Runtime tuning:
async def search_similar_chunks(
embedding: list[float],
top_k: int = 10
) -> list[Chunk]:
"""Vector similarity search with HNSW index."""
# Tune ef_search for accuracy vs speed trade-off
await session.execute(text("SET hnsw.ef_search = 64;"))
results = await session.execute(
select(Chunk)
.order_by(Chunk.embedding.cosine_distance(embedding))
.limit(top_k)
)
return results.scalars().all()Results
Performance:
- Query latency: 85ms → 5ms (17x faster)
- Accuracy: 95% → 98% (3% improvement)
- Build time: 2s → 8s (acceptable for 415 chunks)
Impact on retrieval:
- Hybrid search latency: 95ms → 15ms (p95)
- Throughput: 10.5 req/s → 66 req/s (6x improvement)
Implementation effort: 4 hours (index creation + testing)
Win 3: Hybrid Search Ranking Optimization
Problem
Retrieval pass rate: 87.2%, target: >90%
- Expected chunks ranked 6-10 instead of top-5
- RRF fusion not getting enough candidates
- No metadata boosting
Investigation
Golden dataset analysis (203 queries):
# Evaluate current ranking
results = []
for query in golden_queries:
retrieved = await hybrid_search(query.text, top_k=10)
expected_in_top_k = any(chunk.id in query.expected_chunk_ids for chunk in retrieved)
rank = next((i for i, c in enumerate(retrieved) if c.id in query.expected_chunk_ids), -1)
results.append({
"query": query.text,
"expected_rank": rank,
"found": rank != -1,
"passed": rank < 10
})
# Results:
# Pass rate: 177/203 = 87.2%
# MRR: 0.723Failure analysis:
- 26 queries failed (expected chunk not in top-10)
- Common issue: Expected chunk ranked 11-15
- Root cause: RRF fusion only fetching 2x candidates (20 for top-10)
Solution: Multi-Pronged Optimization
1. Increase RRF Fetch Multiplier
File: backend/app/core/constants.py
# Before
HYBRID_FETCH_MULTIPLIER = 2 # Fetch 20 for top-10
# After
HYBRID_FETCH_MULTIPLIER = 3 # Fetch 30 for top-10Rationale: More candidates → better RRF coverage → higher recall
2. Add Metadata Boosting
File: backend/app/shared/services/search/search_service.py
def apply_metadata_boosts(
chunks: list[Chunk],
query: str
) -> list[Chunk]:
"""Boost scores based on metadata signals."""
query_lower = query.lower()
for chunk in chunks:
# Boost if query matches section title
if chunk.section_title and any(
term in chunk.section_title.lower()
for term in query_lower.split()
):
chunk.score *= SECTION_TITLE_BOOST_FACTOR # 2.0
# Boost if query matches document path
if chunk.document_path and any(
term in chunk.document_path.lower()
for term in query_lower.split()
):
chunk.score *= DOCUMENT_PATH_BOOST_FACTOR # 1.15
# Boost code blocks for technical queries
if chunk.chunk_type == "code_block" and is_technical_query(query):
chunk.score *= TECHNICAL_KEYWORD_BOOST # 1.2
return sorted(chunks, key=lambda c: c.score, reverse=True)3. Pre-Compute tsvector for BM25
Before:
-- Compute tsvector on-the-fly (slow!)
SELECT *, ts_rank(to_tsvector('english', content), query) as rank
FROM chunks
WHERE to_tsvector('english', content) @@ query
ORDER BY rank DESC;After:
-- Use pre-computed tsvector column (fast!)
SELECT *, ts_rank(content_tsvector, query) as rank
FROM chunks
WHERE content_tsvector @@ query
ORDER BY rank DESC;Migration:
def upgrade():
"""Add pre-computed tsvector column."""
# Add column
op.add_column('chunks', sa.Column('content_tsvector', TSVECTOR))
# Populate
op.execute("""
UPDATE chunks
SET content_tsvector = to_tsvector('english', content);
""")
# Create GIN index
op.execute("""
CREATE INDEX idx_chunk_tsvector
ON chunks USING GIN(content_tsvector);
""")
# Add trigger to keep it updated
op.execute("""
CREATE TRIGGER tsvector_update BEFORE INSERT OR UPDATE
ON chunks FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(content_tsvector, 'pg_catalog.english', content);
""")Results
Ranking Quality:
| Metric | Before | After | Change |
|---|---|---|---|
| Pass rate | 177/203 (87.2%) | 186/203 (91.6%) | +5.1% |
| MRR (overall) | 0.723 | 0.777 | +7.4% |
| MRR (hard queries) | 0.647 | 0.686 | +6.0% |
Query Performance:
| Operation | Before | After | Change |
|---|---|---|---|
| BM25 search | 45ms | 4ms | 11x faster |
| Vector search | 5ms | 5ms | Same |
| RRF fusion | 2ms | 3ms | Slightly slower (more candidates) |
| Total | 52ms | 12ms | 4.3x faster |
Impact by boost factor:
- Section title boost: +7.4% MRR (most impactful)
- Document path boost: +2.1% MRR
- Code block boost: +1.3% MRR (for technical queries)
Implementation effort: 1 day (constants, migration, testing)
Win 4: SSE Event Buffering (Race Condition Fix)
Problem
Frontend showed 0% progress while backend was running
- Real-time progress updates missing
- EventSource connection established AFTER events published
- No event replay mechanism
Investigation
Reproduce issue: 1. Start analysis via API 2. Frontend subscribes to SSE /progress/{analysis_id} 3. Backend immediately publishes "analysis_started" event 4. Frontend connects 200ms later → misses early events
Root cause:
# ❌ BAD: Events lost if no subscriber yet
class EventBroadcaster:
def publish(self, channel: str, event: dict):
if channel not in self._subscribers:
return # Event lost!
for subscriber in self._subscribers[channel]:
subscriber.send(event)Solution: Event Buffering with Replay
File: backend/app/services/event_broadcaster.py
from collections import deque
from dataclasses import dataclass
from datetime import datetime
@dataclass
class BufferedEvent:
"""Event with timestamp for replay."""
data: dict
timestamp: datetime
class EventBroadcaster:
"""SSE broadcaster with event buffering."""
def __init__(self, buffer_size: int = 100):
self._subscribers: dict[str, list] = {}
self._buffers: dict[str, deque[BufferedEvent]] = {}
self._buffer_size = buffer_size
def publish(self, channel: str, event: dict):
"""Publish event and store in buffer."""
# Create buffer if needed
if channel not in self._buffers:
self._buffers[channel] = deque(maxlen=self._buffer_size)
# Add to buffer
buffered_event = BufferedEvent(
data=event,
timestamp=datetime.now()
)
self._buffers[channel].append(buffered_event)
# Send to active subscribers
for subscriber in self._subscribers.get(channel, []):
try:
subscriber.send(event)
except Exception as e:
logger.error("failed_to_send_event", error=str(e))
async def subscribe(self, channel: str):
"""Subscribe to channel and replay buffered events."""
# Replay buffered events first
for buffered_event in self._buffers.get(channel, []):
yield {
"event": "message",
"data": json.dumps(buffered_event.data)
}
# Then stream new events
queue = asyncio.Queue()
self._subscribers.setdefault(channel, []).append(queue)
try:
while True:
event = await queue.get()
yield {
"event": "message",
"data": json.dumps(event)
}
finally:
self._subscribers[channel].remove(queue)API endpoint:
@app.get("/progress/{analysis_id}")
async def stream_progress(analysis_id: str):
"""Stream analysis progress with buffered event replay."""
channel = f"analysis:{analysis_id}"
async def event_generator():
async for event in event_broadcaster.subscribe(channel):
yield f"data: {event['data']}\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream"
)Results
Before (with race condition):
- 0% progress shown until agent completion (30-60 seconds)
- Users confused, thought app was frozen
- Support tickets: "Analysis stuck at 0%"
After (with buffering):
- All events delivered (100% replay rate)
- Progress updates appear immediately
- Memory overhead: ~10KB per active analysis (100 events × 100 bytes)
Implementation effort: 3 hours (buffer logic + tests)
Win 5: Quality Gate Content Truncation Fix
Problem
Quality scores artificially low due to content truncation
- Depth scores: 5/10 (AWFUL) → required retries
- G-Eval only seeing truncated summaries
- 4 stages of truncation compounding
Investigation
Trace truncation points:
# Stage 1: compress_findings.py
MAX_STRING_LENGTH = 200 # ❌ Too aggressive!
# Stage 2: scorer.py
input_text = content[:2000] # ❌ Truncated again!
output_text = response[:3000]
# Stage 3: quality.py
MAX_CONTENT_LENGTH = 8000 # ❌ Insufficient!
# Stage 4: quality_gate_node.py
insights = findings[:2000] # ❌ Final truncation!Example: 1. Original finding: 5,000 chars (detailed security analysis) 2. After Stage 1: 200 chars ("Found 3 vulnerabilities...") 3. After synthesis: 1,500 chars (includes other findings) 4. After Stage 2: 1,500 chars (same) 5. After G-Eval: Depth score = 5/10 (insufficient detail)
Solution: Increase All Truncation Limits
Changes:
| File | Before | After | Rationale |
|---|---|---|---|
| compress_findings.py | 200 | 500 | Allow key insights |
| scorer.py (input) | 2,000 | 8,000 | Full context for eval |
| scorer.py (output) | 3,000 | 12,000 | Detailed responses |
| quality.py | 8,000 | 15,000 | Complete synthesis |
| quality_gate_node.py | 2,000 | 8,000 | All findings visible |
Implementation:
# backend/app/shared/services/g_eval/scorer.py
MAX_INPUT_LENGTH = 8000 # Increased from 2000
MAX_OUTPUT_LENGTH = 12000 # Increased from 3000
# backend/app/evaluation/evaluators/quality.py
MAX_CONTENT_LENGTH = 15000 # Increased from 8000
# backend/app/domains/analysis/workflows/tasks/aggregation/compress_findings.py
MAX_STRING_LENGTH = 500 # Increased from 200Results
Quality Scores:
| Criterion | Before | After | Change |
|---|---|---|---|
| Completeness | 0.75 | 0.85 | +13% |
| Accuracy | 0.88 | 0.92 | +5% |
| Coherence | 0.84 | 0.88 | +5% |
| Depth | 0.58 | 0.78 | +34% |
| Overall | 0.76 | 0.86 | +13% |
Pass rate: 67-77% (variable) → 85%+ (stable)
Trade-offs:
- Token usage: +15% (from 8k → 12k avg)
- Cost impact: +$0.02 per analysis (acceptable)
- Quality improvement: Worth the extra cost
Implementation effort: 2 hours (find all truncation points + update tests)
Summary Table
| Optimization | Metric | Before | After | Improvement | Effort |
|---|---|---|---|---|---|
| Multi-level caching | Annual cost | $35k | $2-5k | 85-95% | 2 days |
| HNSW index | Query latency | 85ms | 5ms | 17x faster | 4 hours |
| Hybrid search | Pass rate | 87.2% | 91.6% | +5.1% | 1 day |
| SSE buffering | Event delivery | 60% | 100% | +67% | 3 hours |
| Content truncation | Depth score | 0.58 | 0.78 | +34% | 2 hours |
Total implementation time: 4 days Annual cost savings: $30-33k Quality improvement: 13% overall, 34% depth
References
- OrchestKit Quality Initiative
- Redis Connection Keepalive
- Hybrid Search Constants
- Template:
../scripts/caching-patterns.ts - Template:
../scripts/database-optimization.ts
{
"version": "2.0.0",
"organization": "OrchestKit",
"date": "February 2026",
"abstract": "Performance optimization patterns covering Core Web Vitals, React render optimization, lazy loading, image optimization, backend profiling, and LLM inference.",
"ruleCount": 18,
"categories": 6,
"consolidatedFrom": [
"performance-optimization",
"core-web-vitals",
"render-optimization",
"lazy-loading-patterns",
"image-optimization",
"high-performance-inference"
]
}
Caching Strategies
Multi-level caching patterns for performance optimization.
Cache Hierarchy
L1: In-Memory (LRU, memoization) - fastest, per-process
L2: Distributed (Redis/Memcached) - shared across instances
L3: CDN (edge, static assets) - global, closest to user
L4: Database (materialized views) - fallback, queryableCache-Aside Pattern (Read-Through)
Most common caching pattern:
async function getAnalysis(id: string): Promise<Analysis> {
const cacheKey = `analysis:${id}`;
// Try cache first (L2)
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - fetch from database (L4)
const analysis = await db.query('SELECT * FROM analyses WHERE id = $1', [id]);
// Store in cache for future requests
await redis.setex(cacheKey, 3600, JSON.stringify(analysis)); // 1 hour TTL
return analysis;
}Write-Through Pattern
Update cache when writing to database:
async function updateAnalysis(id: string, updates: Partial<Analysis>) {
// Update database
const updated = await db.query(
'UPDATE analyses SET ... WHERE id = $1 RETURNING *',
[id]
);
// Update cache immediately
const cacheKey = `analysis:${id}`;
await redis.setex(cacheKey, 3600, JSON.stringify(updated));
return updated;
}Cache Invalidation Strategies
1. Time-Based (TTL)
// Short TTL for frequently changing data
await redis.setex('trending:articles', 300, data); // 5 min
// Long TTL for static data
await redis.setex('user:profile:123', 86400, data); // 24 hours2. Event-Based
// Invalidate when data changes
async function deleteAnalysis(id: string) {
await db.query('DELETE FROM analyses WHERE id = $1', [id]);
// Invalidate all related cache keys
await redis.del(`analysis:${id}`);
await redis.del(`analysis:${id}:chunks`);
await redis.del('analysis:list:recent'); // List cache
}3. Tag-Based
// Tag related cache entries
await redis.set('analysis:123', data);
await redis.sadd('tag:user:456', 'analysis:123');
// Invalidate all entries with tag
async function invalidateUserData(userId: string) {
const keys = await redis.smembers(`tag:user:${userId}`);
if (keys.length > 0) {
await redis.del(...keys);
await redis.del(`tag:user:${userId}`);
}
}Redis Patterns
1. String Cache (Most Common)
// Get/set
await redis.set('key', 'value');
const value = await redis.get('key');
// With TTL
await redis.setex('key', 3600, 'value');
// Atomic increment
await redis.incr('page:views:123');2. Hash Cache (Objects)
// Store object fields separately
await redis.hset('user:123', 'name', 'Alice');
await redis.hset('user:123', 'email', 'alice@example.com');
// Get specific field
const name = await redis.hget('user:123', 'name');
// Get all fields
const user = await redis.hgetall('user:123');3. List Cache (Queues, Recent Items)
// Recent analyses (FIFO)
await redis.lpush('analyses:recent', analysisId);
await redis.ltrim('analyses:recent', 0, 99); // Keep only 100 most recent
// Get recent
const recent = await redis.lrange('analyses:recent', 0, 9); // First 104. Set Cache (Unique Items, Tags)
// Track unique visitors
await redis.sadd('article:123:visitors', userId);
// Check membership
const hasVisited = await redis.sismember('article:123:visitors', userId);
// Count unique
const uniqueCount = await redis.scard('article:123:visitors');In-Memory Cache (L1)
For per-process caching:
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, Analysis>({
max: 500, // Maximum items
ttl: 1000 * 60 * 5, // 5 minutes
updateAgeOnGet: true, // Refresh on access
});
function getAnalysis(id: string): Analysis {
// Check L1 first
if (cache.has(id)) {
return cache.get(id)!;
}
// Fetch from L2 or database
const analysis = await fetchAnalysis(id);
cache.set(id, analysis);
return analysis;
}HTTP Caching (Browser/CDN)
// Express.js example
app.get('/api/analyses/:id', async (req, res) => {
const analysis = await getAnalysis(req.params.id);
// Cache in browser and CDN for 1 hour
res.set('Cache-Control', 'public, max-age=3600');
// ETag for conditional requests
const etag = generateETag(analysis);
res.set('ETag', etag);
// Return 304 if unchanged
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(analysis);
});Cache Warming
Preload cache before traffic arrives:
async function warmCache() {
// Load hot data
const recentAnalyses = await db.query(
'SELECT * FROM analyses ORDER BY created_at DESC LIMIT 100'
);
// Populate cache
for (const analysis of recentAnalyses) {
await redis.setex(
`analysis:${analysis.id}`,
3600,
JSON.stringify(analysis)
);
}
console.log(`Warmed cache with ${recentAnalyses.length} analyses`);
}
// Run on server startup
await warmCache();Cache Stampede Prevention
Prevent multiple requests from hitting database simultaneously:
const locks = new Map<string, Promise<Analysis>>();
async function getAnalysis(id: string): Promise<Analysis> {
const cacheKey = `analysis:${id}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Check if fetch is already in progress
if (locks.has(cacheKey)) {
return locks.get(cacheKey)!;
}
// Start fetch
const fetchPromise = (async () => {
const analysis = await db.query('SELECT * FROM analyses WHERE id = $1', [id]);
await redis.setex(cacheKey, 3600, JSON.stringify(analysis));
locks.delete(cacheKey); // Clean up
return analysis;
})();
locks.set(cacheKey, fetchPromise);
return fetchPromise;
}Best Practices
1. Cache frequently accessed, slow-to-compute data 2. Use appropriate TTL - shorter for dynamic data 3. Monitor cache hit rate - aim for > 80% 4. Handle cache failures gracefully - always fall back to database 5. Invalidate proactively when data changes 6. Monitor memory usage - set max memory and eviction policy 7. Use compression for large cached values
References
- Redis Best Practices
- HTTP Caching
- See
scripts/caching-patterns.tsfor complete implementation
CC Prompt Cache Optimization Guide
Why This Matters
CC 2.1.72 includes a prompt cache fix in SDK query() that reduces input token costs up to 12x. The cache works by recognizing repeated prefixes in prompts — if the first N tokens of a prompt match a cached entry, only the remaining tokens are billed at full rate.
The Golden Rule
Stable content FIRST, variable content LAST.
Prompt Structure Template
[1. SYSTEM ROLE & MODE] ← stable, cached across invocations
[2. EVALUATION DIMENSIONS] ← stable
[3. SCORING FORMULA] ← stable
[4. TOOL BUDGET / CONSTRAINTS] ← stable
[5. OUTPUT FORMAT] ← stable
[6. VARIABLE CONTENT] ← unique per invocation (feature, topic, files)Before / After Examples
Bad (cache-hostile):
Agent(prompt=f"""BACKEND ARCH: {feature}
Standards: FastAPI, Pydantic v2...
Deliverables: API, schemas, models...""")Cache reuse: ~10% (variable content invalidates prefix)
Good (cache-friendly):
Agent(prompt=f"""BACKEND ARCHITECTURE DESIGN
STANDARDS: FastAPI, Pydantic v2, SQLAlchemy 2.0 async
DELIVERABLES:
1. API endpoint design
2. Pydantic schemas
3. SQLAlchemy models
...
FEATURE: {feature}""")Cache reuse: ~70% (stable prefix cached, only variable suffix is new)
once:true Hook Pattern
Skills that spawn multiple agents with similar instructions should use once: true hooks to inject stable content once:
hooks:
PreToolUse:
- matcher: "Agent"
command: "${CLAUDE_PLUGIN_ROOT}/hooks/bin/run-hook.mjs skill/standards-loader"
once: true # Inject standards ONCE, all Agent spawns benefitMeasuring Cache Efficiency
- Longer stable prefixes = higher cache hit rate
- Same role + same dimensions across agents = cache hits
- Variable content (feature names, file lists) should be < 30% of prompt
Skills with Highest Cache Benefit
| Skill | Agents | Est. Savings |
|---|---|---|
| implement | 10 | ~400-500 tokens |
| review-pr | 6 | ~270-330 tokens |
| verify | 6 | ~210-270 tokens |
| fix-issue | 5 | ~150-175 tokens |
| brainstorm | 4 | ~100-120 tokens |
Image CDN Configuration
Complete guide to configuring image CDNs and optimization pipelines.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Image Delivery Pipeline │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Source CDN / Optimizer Browser │
│ ┌────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ Origin │──────────►│ Resize │──AVIF────►│ Chrome │ │
│ │ Server │ │ Format │ │ Safari │ │
│ │ /CMS │ │ Quality │──WebP────►│ Firefox │ │
│ └────────┘ │ Cache │ │ Edge │ │
│ └─────────────┘ └──────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Edge Cache │ │
│ │ (Global) │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘Next.js Remote Patterns
Basic Configuration
// next.config.js
module.exports = {
images: {
// Enable modern formats
formats: ['image/avif', 'image/webp'],
// Allowed remote sources (required for external images)
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: '*.cloudinary.com',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: 's3.amazonaws.com',
pathname: '/my-bucket/**',
},
],
// Responsive breakpoints
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Cache TTL (seconds) - default 60, increase for CDN
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
// Disable optimization in development (faster builds)
unoptimized: process.env.NODE_ENV === 'development',
},
};Environment-Based Configuration
// next.config.js
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
// Different patterns per environment
remotePatterns: [
// Production CDN
...(isProd
? [
{
protocol: 'https',
hostname: 'cdn.example.com',
},
]
: []),
// Development/staging
...(!isProd
? [
{
protocol: 'https',
hostname: 'staging-cdn.example.com',
},
{
protocol: 'http',
hostname: 'localhost',
port: '3001',
},
]
: []),
],
},
};Cloudinary Integration
Loader Implementation
// lib/loaders/cloudinary.ts
import type { ImageLoader } from 'next/image';
const CLOUD_NAME = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
export const cloudinaryLoader: ImageLoader = ({ src, width, quality }) => {
// Build transformation string
const transforms = [
`w_${width}`,
`q_${quality || 'auto:good'}`,
'f_auto', // Auto format (AVIF > WebP > JPEG)
'c_limit', // Don't upscale
'dpr_auto', // Auto DPR
].join(',');
// Handle both full URLs and paths
const imagePath = src.startsWith('http')
? src.replace(/^https?:\/\/[^/]+/, '')
: src;
return `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/${transforms}/${imagePath}`;
};
// Advanced loader with more options
export const cloudinaryAdvancedLoader: ImageLoader = ({ src, width, quality }) => {
const params = new URLSearchParams();
// Responsive width
params.set('w', width.toString());
// Quality (auto:good is a good default)
params.set('q', quality?.toString() || 'auto:good');
// Additional optimizations
const transforms = [
`w_${width}`,
`q_${quality || 'auto:good'}`,
'f_auto', // Best format for browser
'c_limit', // Don't upscale
'fl_progressive', // Progressive loading
'fl_immutable_cache', // Long cache
].join(',');
return `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/${transforms}/${src}`;
};
export default cloudinaryLoader;Usage
import Image from 'next/image';
import { cloudinaryLoader } from '@/lib/loaders/cloudinary';
// Component usage
<Image
loader={cloudinaryLoader}
src="products/shoe-red.jpg" // Path in Cloudinary
alt="Red running shoe"
width={400}
height={400}
sizes="(max-width: 768px) 100vw, 400px"
/>
// Global loader configuration
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/loaders/cloudinary.ts',
},
};Imgix Integration
// lib/loaders/imgix.ts
import type { ImageLoader } from 'next/image';
const IMGIX_DOMAIN = process.env.NEXT_PUBLIC_IMGIX_DOMAIN;
export const imgixLoader: ImageLoader = ({ src, width, quality }) => {
const url = new URL(`https://${IMGIX_DOMAIN}${src}`);
// Auto format negotiation
url.searchParams.set('auto', 'format,compress');
// Width
url.searchParams.set('w', width.toString());
// Quality
url.searchParams.set('q', (quality || 75).toString());
// Fit mode (contain, cover, fill, etc.)
url.searchParams.set('fit', 'max');
return url.toString();
};
// With advanced features
export const imgixAdvancedLoader: ImageLoader = ({ src, width, quality }) => {
const url = new URL(`https://${IMGIX_DOMAIN}${src}`);
url.searchParams.set('auto', 'format,compress');
url.searchParams.set('w', width.toString());
url.searchParams.set('q', (quality || 75).toString());
url.searchParams.set('fit', 'max');
// Face detection for portraits
// url.searchParams.set('fit', 'facearea');
// url.searchParams.set('facepad', '2');
// Blur for placeholders
// url.searchParams.set('blur', '200');
// url.searchParams.set('px', '16');
return url.toString();
};Cloudflare Images
// lib/loaders/cloudflare.ts
import type { ImageLoader } from 'next/image';
// Using Cloudflare Image Resizing
export const cloudflareResizingLoader: ImageLoader = ({ src, width, quality }) => {
// src should be the full URL of the original image
const params = [
`width=${width}`,
`quality=${quality || 85}`,
'format=auto', // Auto AVIF/WebP
'fit=scale-down', // Don't upscale
].join(',');
return `https://yourdomain.com/cdn-cgi/image/${params}/${src}`;
};
// Using Cloudflare Images (upload API)
const ACCOUNT_HASH = process.env.NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_HASH;
export const cloudflareImagesLoader: ImageLoader = ({ src, width }) => {
// src is the image ID from Cloudflare
// Variants are predefined in Cloudflare dashboard
const variant = width <= 640 ? 'small' : width <= 1024 ? 'medium' : 'large';
return `https://imagedelivery.net/${ACCOUNT_HASH}/${src}/${variant}`;
};AWS S3 + CloudFront
// lib/loaders/aws.ts
import type { ImageLoader } from 'next/image';
const CLOUDFRONT_DOMAIN = process.env.NEXT_PUBLIC_CLOUDFRONT_DOMAIN;
// Basic CloudFront loader (requires Lambda@Edge for resizing)
export const cloudfrontLoader: ImageLoader = ({ src, width, quality }) => {
// Lambda@Edge parses these query params
const params = new URLSearchParams({
w: width.toString(),
q: (quality || 80).toString(),
f: 'auto',
});
return `https://${CLOUDFRONT_DOMAIN}${src}?${params}`;
};
// For static S3 images (no resizing)
export const s3Loader: ImageLoader = ({ src }) => {
return `https://${CLOUDFRONT_DOMAIN}${src}`;
};Vercel Image Optimization
// Automatically enabled on Vercel
// Configure in next.config.js
module.exports = {
images: {
// Use Vercel's built-in optimizer
loader: 'default',
// External domains need explicit allowlist
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
},
],
// Increase cache for static images
minimumCacheTTL: 60 * 60 * 24 * 365, // 1 year
},
};
// For non-Vercel deployments, use external loader
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/loaders/cloudinary.ts',
},
};Self-Hosted with Sharp
// For self-hosted Next.js (Docker, Node.js)
// 1. Install Sharp
// npm install sharp
// 2. Configure next.config.js
module.exports = {
images: {
loader: 'default', // Uses Sharp internally
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
// Important for self-hosted
dangerouslyAllowSVG: false,
contentDispositionType: 'attachment',
},
};
// 3. Dockerfile - ensure Sharp can build
FROM node:20-alpine AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]CDN Headers & Caching
Nginx Configuration
# /etc/nginx/conf.d/images.conf
# Image caching
location ~* \.(jpg|jpeg|png|webp|avif|gif|ico|svg)$ {
# Long cache for immutable assets
expires 1y;
add_header Cache-Control "public, immutable";
# Vary by Accept header for format negotiation
add_header Vary "Accept";
# Security headers
add_header X-Content-Type-Options "nosniff";
}
# Next.js optimized images
location /_next/image {
proxy_pass http://nextjs_upstream;
proxy_cache_valid 200 365d;
# Cache key includes Accept header for format
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept";
add_header X-Cache-Status $upstream_cache_status;
}Cloudflare Page Rules
{
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "*.example.com/*.(jpg|jpeg|png|webp|avif|gif)"
}
}
],
"actions": [
{
"id": "cache_level",
"value": "cache_everything"
},
{
"id": "edge_cache_ttl",
"value": 2592000
},
{
"id": "browser_cache_ttl",
"value": 31536000
},
{
"id": "polish",
"value": "lossless"
}
]
}Blur Placeholder Generation
Build-Time with Plaiceholder
// lib/blur.ts
import { getPlaiceholder } from 'plaiceholder';
import fs from 'fs/promises';
import path from 'path';
export async function getBlurDataURL(imagePath: string): Promise<string> {
try {
const file = await fs.readFile(path.join(process.cwd(), 'public', imagePath));
const { base64 } = await getPlaiceholder(file);
return base64;
} catch {
// Return a tiny transparent placeholder on error
return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
}
}
// Usage in getStaticProps
export async function getStaticProps() {
const blurDataURL = await getBlurDataURL('/images/hero.jpg');
return {
props: { blurDataURL },
};
}Remote Image Blur
// lib/remote-blur.ts
import { getPlaiceholder } from 'plaiceholder';
export async function getRemoteBlurDataURL(imageUrl: string): Promise<string> {
try {
const response = await fetch(imageUrl);
const buffer = Buffer.from(await response.arrayBuffer());
const { base64 } = await getPlaiceholder(buffer);
return base64;
} catch {
return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
}
}
// Cache blur data URLs
const blurCache = new Map<string, string>();
export async function getCachedBlurDataURL(imageUrl: string): Promise<string> {
if (blurCache.has(imageUrl)) {
return blurCache.get(imageUrl)!;
}
const blur = await getRemoteBlurDataURL(imageUrl);
blurCache.set(imageUrl, blur);
return blur;
}Image Validation & Error Handling
// lib/image-validation.ts
export function isValidImageUrl(url: string): boolean {
try {
const parsed = new URL(url);
const allowedHosts = ['cdn.example.com', 'images.unsplash.com'];
return allowedHosts.some(
(host) => parsed.hostname === host || parsed.hostname.endsWith(`.${host}`)
);
} catch {
return false;
}
}
export function getOptimizedImageUrl(
src: string,
options: { width: number; quality?: number }
): string {
// Use your CDN loader
const { width, quality = 80 } = options;
if (src.includes('cloudinary.com')) {
return src.replace('/upload/', `/upload/w_${width},q_${quality},f_auto/`);
}
// Default: return as-is
return src;
}Core Web Vitals Optimization
Google's Core Web Vitals are the key metrics for measuring user experience.
The Three Metrics
| Metric | Target | Measures | Impact |
|---|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | Loading performance | First impression |
| INP (Interaction to Next Paint) | < 200ms | Responsiveness | User frustration |
| CLS (Cumulative Layout Shift) | < 0.1 | Visual stability | Accidental clicks |
LCP (Largest Contentful Paint)
What It Measures
Time until the largest visible element (hero image, heading, video) renders.
Common Causes
- Large, unoptimized images
- Slow server response time (TTFB > 600ms)
- Render-blocking JavaScript/CSS
- Client-side rendering
Fixes
1. Optimize Images
<!-- Preload LCP image -->
<link rel="preload" as="image" href="/hero.jpg" />
<!-- Use modern formats -->
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero" width="1200" height="600" />
</picture>
<!-- Or use next/image -->
<Image src="/hero.jpg" priority quality={85} />2. Reduce Server Response Time
- Use CDN for static assets
- Enable HTTP/2 or HTTP/3
- Optimize database queries
- Implement caching (Redis, CDN)
3. Eliminate Render-Blocking Resources
<!-- Defer non-critical CSS -->
<link rel="preload" as="style" href="/styles.css" onload="this.onload=null;this.rel='stylesheet'" />
<!-- Defer JavaScript -->
<script src="/app.js" defer></script>
<!-- Inline critical CSS -->
<style>
/* Critical above-the-fold styles */
.hero { ... }
</style>4. Use Server-Side Rendering (SSR)
// Next.js SSR
export async function getServerSideProps() {
const data = await fetchData();
return { props: { data } };
}
// React Server Components
async function Page() {
const data = await fetchData(); // Runs on server
return <div>{data}</div>;
}INP (Interaction to Next Paint)
What It Measures
Time from user interaction (click, tap, key press) to visual feedback.
Common Causes
- Heavy JavaScript execution blocking main thread
- Long-running event handlers
- Expensive DOM updates
- Third-party scripts
Fixes
1. Debounce/Throttle Expensive Operations
import { debounce } from 'lodash';
// Without debounce: runs on EVERY keystroke
function handleSearch(query: string) {
const results = expensiveSearch(query); // Blocks for 100ms
setResults(results);
}
// With debounce: runs 300ms after user stops typing
const handleSearch = debounce((query: string) => {
const results = expensiveSearch(query);
setResults(results);
}, 300);2. Use Web Workers for Heavy Computation
// worker.ts
self.onmessage = (e) => {
const result = expensiveComputation(e.data);
self.postMessage(result);
};
// main.ts
const worker = new Worker('/worker.js');
worker.postMessage(data);
worker.onmessage = (e) => {
setResult(e.data);
};3. Split Long Tasks
// Before: Blocks main thread for 500ms
function processItems(items) {
items.forEach(item => {
processItem(item); // 5ms each × 100 items = 500ms
});
}
// After: Yields to browser between batches
async function processItems(items) {
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
batch.forEach(processItem);
// Yield to browser
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// Or use Scheduler API (modern)
async function processItems(items) {
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
batch.forEach(processItem);
await scheduler.yield(); // Yield to higher priority tasks
}
}4. Optimize React Rendering
// Memoize expensive components
const Chart = memo(({ data }) => <ExpensiveChart data={data} />);
// Use startTransition for non-urgent updates
import { useTransition } from 'react';
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // Urgent: update input immediately
startTransition(() => {
// Non-urgent: can be interrupted
const filtered = filterResults(e.target.value);
setResults(filtered);
});
}
return <input value={query} onChange={handleChange} />;
}CLS (Cumulative Layout Shift)
What It Measures
Visual stability - how much elements unexpectedly shift during load.
Common Causes
- Images without dimensions
- Ads/embeds injected after layout
- Web fonts causing FOIT/FOUT
- Dynamically injected content
Fixes
1. Always Set Image Dimensions
<!-- ❌ BAD: No dimensions, causes layout shift -->
<img src="/photo.jpg" alt="Photo" />
<!-- ✅ GOOD: Reserves space -->
<img src="/photo.jpg" alt="Photo" width="800" height="600" />
<!-- Or with aspect ratio (CSS) -->
<img src="/photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;" />2. Reserve Space for Ads/Embeds
.ad-container {
min-height: 250px; /* Reserve space before ad loads */
background: #f0f0f0;
}3. Optimize Web Font Loading
/* Prevent FOIT (flash of invisible text) */
@font-face {
font-family: 'CustomFont';
src: url('/font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when ready */
}<!-- Preload critical fonts -->
<link rel="preload" as="font" href="/font.woff2" type="font/woff2" crossorigin />4. Avoid Inserting Content Above Existing Content
// ❌ BAD: Inserts notification at top, shifts everything down
function addNotification(message) {
container.insertAdjacentHTML('afterbegin', `<div>${message}</div>`);
}
// ✅ GOOD: Append to bottom or use fixed positioning
function addNotification(message) {
const notification = document.createElement('div');
notification.className = 'notification-fixed'; // position: fixed
notification.textContent = message;
document.body.appendChild(notification);
}Measuring Core Web Vitals
In Development
// Use web-vitals library
import { onCLS, onINP, onLCP } from 'web-vitals';
onLCP(console.log); // Log LCP
onINP(console.log); // Log INP
onCLS(console.log); // Log CLSIn Production (RUM - Real User Monitoring)
import { onCLS, onINP, onLCP } from 'web-vitals';
function sendToAnalytics(metric) {
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(metric),
});
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);Lighthouse (Lab Testing)
# Run Lighthouse audit
lighthouse https://your-site.com --output=html
# Or use Chrome DevTools
# Open DevTools → Lighthouse tab → Generate reportTargets by Percentile
Google measures at the 75th percentile of all page loads:
| Grade | LCP | INP | CLS |
|---|---|---|---|
| Good (Green) | < 2.5s | < 200ms | < 0.1 |
| Needs Improvement (Orange) | 2.5-4s | 200-500ms | 0.1-0.25 |
| Poor (Red) | > 4s | > 500ms | > 0.25 |
Goal: 75% of page loads should be "Good" for all three metrics.
Quick Wins Checklist
- [ ] Add
widthandheightto all images - [ ] Preload LCP image
- [ ] Use
font-display: swapfor web fonts - [ ] Defer non-critical JavaScript
- [ ] Enable HTTP/2 and compression
- [ ] Use CDN for static assets
- [ ] Implement lazy loading for below-fold images
- [ ] Memoize expensive React components
- [ ] Debounce search inputs and expensive handlers
References
Database Query Optimization
Strategies for optimizing database performance and eliminating slow queries.
Key Patterns
1. Add Missing Indexes - Turn Seq Scan into Index Scan 2. Fix N+1 Queries - Use JOINs or include instead of loops 3. Cursor Pagination - Never load all records 4. Connection Pooling - Manage connection lifecycle
Quick Diagnostics
-- Find slow queries (PostgreSQL)
SELECT query, calls, mean_time / 1000 as mean_seconds
FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;
-- Verify index usage
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
-- Check for sequential scans
SELECT schemaname, tablename, seq_scan, seq_tup_read
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 10;N+1 Query Detection
Symptoms:
- One query to get parent records, then N queries for related data
- Rapid sequential database calls in logs
- Linear growth in query count with data size
Example Problem:
# ❌ BAD: N+1 query (1 + 8 queries)
analyses = await session.execute(select(Analysis).limit(8)).scalars().all()
for analysis in analyses:
# Each iteration hits DB again!
chunks = await session.execute(
select(Chunk).where(Chunk.analysis_id == analysis.id)
).scalars().all()Solution:
# ✅ GOOD: Single query with JOIN (1 query)
from sqlalchemy.orm import selectinload
analyses = await session.execute(
select(Analysis)
.options(selectinload(Analysis.chunks)) # Eager load
.limit(8)
).scalars().all()
# Now analyses[0].chunks is already loaded (no extra query)Index Selection Strategies
| Index Type | Use Case | Example |
|---|---|---|
| B-tree | Equality, range queries | WHERE created_at > '2025-01-01' |
| GIN | Full-text search, JSONB | WHERE content_tsvector @@ to_tsquery('python') |
| HNSW | Vector similarity | ORDER BY embedding <=> '[0.1, 0.2, ...]' |
| Hash | Exact equality only | WHERE id = 'abc123' (rare) |
Index Creation Examples:
-- B-tree index for range queries
CREATE INDEX idx_analyses_created_at ON analyses(created_at);
-- GIN index for full-text search
CREATE INDEX idx_chunks_tsvector ON chunks USING GIN(content_tsvector);
-- HNSW index for vector similarity
CREATE INDEX idx_chunks_embedding ON chunks
USING hnsw (embedding vector_cosine_ops);
-- Partial index for active records only
CREATE INDEX idx_active_users ON users(email)
WHERE deleted_at IS NULL;
-- Composite index for common query pattern
CREATE INDEX idx_analyses_user_status ON analyses(user_id, status);Connection Pooling
Problem: Creating new connections is expensive (50-100ms overhead)
Solution: Use connection pools
# SQLAlchemy async pool
engine = create_async_engine(
DATABASE_URL,
pool_size=20, # Base connections
max_overflow=10, # Additional if needed
pool_pre_ping=True, # Verify connections are alive
pool_recycle=3600 # Recycle after 1 hour
)Pagination: Cursor vs Offset
Offset-Based (❌ Slow for large datasets)
SELECT * FROM analyses ORDER BY created_at DESC
LIMIT 20 OFFSET 1000; -- Must scan 1020 rows!Cursor-Based (✅ Fast, scales to millions)
SELECT * FROM analyses
WHERE created_at < '2025-01-15 10:00:00' -- Last cursor
ORDER BY created_at DESC
LIMIT 20; -- Only scans 20 rowsBest Practices
1. Always use EXPLAIN ANALYZE before deploying queries 2. Index foreign keys used in JOINs 3. Avoid SELECT \ - request only needed columns 4. Use prepared statements to prevent SQL injection and enable query caching 5. Monitor pg_stat_statements weekly 6. Set query timeouts* to prevent runaway queries
References
- PostgreSQL Performance Tips
- Use The Index, Luke
- See
scripts/database-optimization.tsfor implementation patterns
[Rule Name]
[Brief description — 1-2 sentences.]
Incorrect:
// Bad patternCorrect:
// Good patternKey rules:
- [Rule 1]
- [Rule 2]
- [Rule 3]
Reference: [link]