Performance Optimization Using Cursor
Your SaaS dashboard loads in 6 seconds. The product page takes 3 seconds to become interactive. The API endpoint that powers the billing dashboard has a p95 latency of 2.4 seconds, and the database CPU spikes to 80% every time someone opens the “Usage Analytics” tab. Users are complaining. Your CTO wants a 50% improvement by end of month.
Performance optimization is a diagnosis-first discipline. The biggest waste of time is optimizing the wrong thing — shaving 50ms off a render that was never the bottleneck while the real problem is a database query that runs 47 times per page load. Cursor’s strength here is its ability to analyze large amounts of profiling data, spot patterns across hundreds of lines of metrics output, and suggest targeted fixes with specific before/after expectations.
What You’ll Walk Away With
Section titled “What You’ll Walk Away With”- A profiling workflow that identifies the actual bottleneck before you touch any code
- A prompt for analyzing database query logs and catching N+1 patterns automatically
- A bundle analysis prompt that finds the largest dependencies and suggests tree-shaking opportunities
- A React rendering optimization prompt that identifies unnecessary re-renders and memoization opportunities
- A before/after benchmarking technique that proves your optimization actually worked
The Workflow
Section titled “The Workflow”Step 1: Measure before optimizing
Section titled “Step 1: Measure before optimizing”The cardinal rule of performance work: measure first. Use Ask mode to set up profiling so you have a baseline to compare against.
Run your application with profiling enabled. Load the pages that are slow. Collect at least 5 minutes of data or 100 requests, whichever is more.
Step 2: Identify the bottleneck
Section titled “Step 2: Identify the bottleneck”Feed the profiling data into Ask mode and let the AI find the pattern.
In most applications, one of these three is the bottleneck: N+1 database queries, missing indexes, or overly broad queries that fetch more data than needed. The AI will identify which one and point you to the specific code.
Step 3: Fix N+1 queries
Section titled “Step 3: Fix N+1 queries”N+1 queries are the most common backend performance problem. Instead of loading related data in a single query with a JOIN, the code loads the parent records and then runs a separate query for each child record.
@src/api/billing/usage.ts @src/db/schema.ts
This endpoint loads organization usage data with an N+1 pattern:- First query: SELECT * FROM organizations WHERE plan = 'enterprise'- Then for EACH org: SELECT SUM(amount) FROM usage WHERE org_id = ? AND month = ?
This executes 1 + N queries where N is the number of enterprise organizations (currently 340).
Rewrite this to use a single query with a JOIN and GROUP BY.Requirements:- The response format must stay exactly the same (don't break the frontend)- Use Drizzle's query builder, not raw SQL- Add an index on usage(org_id, month) if it doesn't already exist- Log the query execution time before and after so I can verify the improvementStep 4: Optimize the frontend bundle
Section titled “Step 4: Optimize the frontend bundle”For frontend performance, bundle size is often the biggest lever. A 500KB JavaScript bundle takes seconds to parse on a mid-range phone.
Step 5: Fix React rendering performance
Section titled “Step 5: Fix React rendering performance”If the frontend is slow despite a reasonable bundle size, the problem is usually unnecessary re-renders. The React DevTools Profiler can show you which components re-render on every state change, but analyzing that output is tedious. Feed it to Cursor.
@src/components/billing-dashboard.tsx @src/components/usage-chart.tsx @src/hooks/use-billing-data.ts
The billing dashboard re-renders the entire page every time the usage chart tooltipappears (mouse hover). The React Profiler shows these components re-rendering:- BillingDashboard (40ms)- UsageChart (25ms)- InvoiceList (15ms) -- this one should NOT re-render on chart hover- UsageSummary (10ms) -- this one should NOT re-render either
Fix the unnecessary re-renders:
1. Identify which state change is triggering the full re-render2. Split the tooltip state so it only triggers re-render in UsageChart3. Memoize InvoiceList and UsageSummary with React.memo (add proper comparison)4. If the billing data hook is causing re-renders, stabilize its return value with useMemo5. Do NOT memoize everything blindly -- only the components that are expensive to re-render
Explain your reasoning for each change. Wrong memoization is worse than no memoization.Step 6: Verify with benchmarks
Section titled “Step 6: Verify with benchmarks”After optimization, prove the improvement with numbers. Do not just “feel like it’s faster.”
@src/api/billing/usage.ts @src/components/billing-dashboard.tsx
Create a before/after performance comparison:
Backend:1. Run 100 requests against the billing endpoint2. Report: median, p95, p99 latency3. Report: database query count per request4. Report: total database time per request
Frontend:1. Run a Lighthouse performance audit on the billing dashboard2. Report: LCP, TBT, CLS, Speed Index3. Report: JavaScript bundle size4. Report: number of render cycles on initial page load
Compare against the baseline measurements from Step 1.Format as a table showing before, after, and percentage improvement.When This Breaks
Section titled “When This Breaks”Premature optimization of non-bottlenecks. The AI will happily optimize anything you point it at, whether or not it matters. Always start with profiling data. If a function takes 2ms and the total request takes 2000ms, optimizing that function is a waste of time.
Memoization that makes things slower. React.memo, useMemo, and useCallback have overhead. If the component is cheap to re-render (simple JSX, no expensive calculations), adding memoization adds memory pressure and comparison cost for zero benefit. Only memoize components that are measurably expensive.
Database indexes that slow writes. Every index speeds up reads but slows writes. For a table that receives 10,000 writes per second (like a usage tracking table), adding 5 indexes means 5 additional B-tree insertions per write. Use Ask mode to analyze the read/write ratio before adding indexes.
Bundle splitting that increases total download size. Lazy-loading a component adds a new HTTP request and a separate chunk. If the component is small (under 10KB) and loaded on most page views, the overhead of the additional request exceeds the savings. Only split chunks that are large AND rarely loaded.
Optimized code that is harder to maintain. A hand-rolled SQL query with 4 subqueries and 3 CTEs might be faster than the ORM query, but if nobody on the team can understand or modify it, you have traded runtime performance for development velocity. Use the ORM by default and drop to raw SQL only for proven hot paths with measured performance requirements.