Application performance optimization with Cursor
PLApplication performance optimization with Cursor is a measure-first loop: record a baseline with real tools, have the agent rank bottlenecks from that data, fix one bottleneck per change (N+1 queries, bundle weight or re-renders), and accept the change only when a rerun of the same measurement, an unchanged test suite and a CI budget all confirm the gain.
This page is for the developer who owns a slow feature and the tech lead who has to approve the fix. Your SaaS billing dashboard takes 6 seconds to load, its usage endpoint has a p95 of 2.4 seconds, and database CPU jumps every time someone opens the Usage analytics tab. The agent will happily “optimize” anything you point it at, so the risk is not a lack of changes. The risk is a plausible diff that makes nothing faster, or makes it faster and wrong.
Scope: this page covers your application’s code. If Cursor itself is slow (indexing, extensions, memory), see tuning Cursor for large projects. For production infrastructure problems such as pool exhaustion or autoscaler flapping, see production performance optimization.
What you get from a measure-first performance loop
Section titled “What you get from a measure-first performance loop”- A baseline folder (
perf/baseline/) produced by commands you run, so no number in the PR comes from the model’s imagination - Two read-only prompts that turn raw timings, query logs and traces into a ranked list of bottlenecks
- Fix prompts for N+1 queries, bundle weight and React re-renders that each carry their own guard test
- A CI bundle budget and a query-count test that keep the win from quietly regressing
- A before/after table a reviewer can approve without reading every changed line
Which tool measures which symptom?
Section titled “Which tool measures which symptom?”Pick the measurement by symptom before you write a prompt. The agent is good at reading large outputs; it is not a profiler, and a number it “reports” without a file behind it is a guess.
| Symptom | Measure with | What the agent reads |
|---|---|---|
| Slow API endpoint, high p95 | autocannon 8.0.0 load run with -j JSON output | perf/baseline/*-load.json |
| Database CPU spikes, many queries per request | The ORM’s query logger (for Drizzle, logger: true) plus a per-request query counter | Query log grouped by request ID |
| Slow first load, large JavaScript | npx next experimental-analyze (Next.js 16.1+, Turbopack only) or @next/bundle-analyzer 16.3.6 (webpack builds) | Analyzer output and chunk sizes |
| Poor LCP, INP or CLS in the browser | Lighthouse 13.5.0 CLI, or a trace through the Chrome DevTools MCP server | Lighthouse JSON, trace insights |
| Janky interaction after load | React DevTools Profiler, exported with Save profile | The exported profile JSON |
Versions were checked on npm on 2026-09-26. The web-vitals package (6.2.2) exports onCLS, onFCP, onINP, onLCP and onTTFB and no onFID, so any generated code that calls onFID is out of date.
How to capture a baseline the agent cannot invent
Section titled “How to capture a baseline the agent cannot invent”Run the measurements yourself (or let the agent run them in the terminal) and save the raw output to files. Every later comparison reads those files, never the chat history. Measure a production build: a dev server’s timings are dominated by on-demand compilation.
# Terminal, against a production build (npm run build && npm run start)mkdir -p perf/baselinenpx autocannon -c 20 -d 30 -j "http://localhost:3000/api/billing/usage?month=2026-09" \ > perf/baseline/usage-load.jsonnpx lighthouse http://localhost:3000/billing --only-categories=performance \ --preset=desktop --output=json --output-path=perf/baseline/billing-lighthouse.json --quietThe load run needs a request-level view too: how many queries each request runs and how long they take. That instrumentation is the first thing to ask the agent for.
Once the instrumentation is in, capture the request log. The baseline block above ran before it existed, so rerun the load test with logging on:
# Terminal 1: production server with request logging; keep only the JSON linesPERF_LOGGING=1 npm run start --silent | grep --line-buffered '^{' > perf/baseline/requests.jsonl
# Terminal 2: rerun the baseline load test against itnpx autocannon -c 20 -d 30 -j "http://localhost:3000/api/billing/usage?month=2026-09" \ > perf/baseline/usage-load.json--silent drops npm’s script banner, and the grep drops the startup lines next start prints, so jq later reads pure JSON Lines instead of failing on the first non-JSON line. Collect at least 100 requests to the slow endpoints in perf/baseline/requests.jsonl. autocannon reports p97.5 and p99, not p95; p95 comes from requests.jsonl. Commit the perf/baseline/ folder on the branch; it is the evidence the reviewer compares against.
For browser work, the Chrome DevTools MCP server (npm chrome-devtools-mcp 1.10.1, maintained by the Chrome DevTools team) lets the agent record a performance trace (performance_start_trace / performance_stop_trace) and read its insights (performance_analyze_insight). Its lighthouse_audit tool excludes the performance category (checked in 1.10.1), so keep the Lighthouse CLI run for LCP and TBT scores. Add it to .cursor/mcp.json (the shape comes from the vendor README; see MCP setup in Cursor):
{ "mcpServers": { "chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@latest", "--isolated", "--no-usage-statistics", "--no-performance-crux"] } }}The server sends usage statistics to Google unless you pass --no-usage-statistics, and its performance tools can send trace URLs to the CrUX API unless you add --no-performance-crux. Add both flags for internal URLs. Each connected server also adds its tool definitions to the agent’s context; see managing tokens in Cursor.
How do you find the real bottleneck from profiling data?
Section titled “How do you find the real bottleneck from profiling data?”Ask for analysis in Plan Mode, which produces a plan before any code is written. The goal of this step is a ranked list with a confirmation test for each item, not a fix.
Check two numbers from the agent’s answer against the raw file yourself, for example the p95 of one endpoint:
# Terminal: nearest-rank p95 of duration_ms for one endpointjq -s '[.[] | select(.path=="/api/billing/usage").duration_ms] | sort | .[((length*0.95)|ceil) - 1]' perf/baseline/requests.jsonlIf they disagree, the agent sampled or misread the log, and the ranking is not trustworthy until it recomputes. When a ranked item names a specific cause, confirm it before the fix: an N+1 claim is confirmed by one request’s log lines, a bundle claim by the analyzer output.
How to fix N+1 queries without changing the response
Section titled “How to fix N+1 queries without changing the response”An N+1 query loads parent rows and then runs one query per row. In the billing scenario the usage endpoint runs one query for enterprise organizations and then one SUM(amount) per organization: 341 queries for 340 organizations.
Before the fix, freeze the behaviour. A snapshot of the current JSON response and a test that counts queries per request turn “the diff looks right” into two assertions that fail if it is not.
The LEFT JOIN line is not decoration. The most common wrong rewrite of this pattern is an inner join, which drops every organization with no usage rows for the month. The response looks right in a demo and silently loses rows in production. The snapshot with a zero-usage organization is what catches it.
How to cut bundle weight and keep it cut
Section titled “How to cut bundle weight and keep it cut”Start from the analyzer, not from a list of libraries you suspect.
# Terminal: interactive Turbopack bundle analyzer; does not produce an app build (Next.js 16.1+, Turbopack only)npx next experimental-analyze# Terminal: requires @next/bundle-analyzer wired into next.config.js# Next.js 16 builds with Turbopack by default; this analyzer only runs with --webpackANALYZE=true npx next build --webpack # writes client.html, edge.html and nodejs.html to .next/analyze/If you want the analysis from a real production build instead, next build --experimental-analyze runs the same analyzer as part of the build (Turbopack only, checked in next 16.3.6).
Paste the largest modules from the report into the prompt. The numbers below are the billing dashboard’s; replace them with yours.
The size-limit budget (14.1.0, brotli-compressed sizes by default) is what keeps the gain. The ceiling sits just above the size you reached, not below it: a limit under the current total fails CI on the very commit that adds it. Without it, the next feature branch brings back a heavy import and nobody notices until Lighthouse scores drift.
How to stop unnecessary React re-renders
Section titled “How to stop unnecessary React re-renders”When the bundle is reasonable and interactions still stutter, record the interaction in the React DevTools Profiler, save the profile, and give the agent the file and the components involved.
Then record the same interaction again and compare the two profiles. A fix that adds React.memo to components that were cheap to begin with is overhead, not optimization.
How to prove the win without reading every line
Section titled “How to prove the win without reading every line”Rerun exactly the baseline commands into perf/after/, collect perf/after/requests.jsonl the same way as the baseline (at least 100 requests to the same endpoints), run the full test suite, and have the agent compare files, not memories.
Paste that table into the PR description. The reviewer then checks evidence instead of reading the diff line by line:
| Evidence | Proves | Who checks it |
|---|---|---|
perf/baseline/ and perf/after/ from the same commands on a production build | The gain is real and measured the same way twice | Tech lead, in the PR |
| Response snapshot test unchanged | The faster code returns the same data | CI |
| Query-count test (at most 2 queries per request) | The N+1 cannot return unnoticed | CI |
size-limit budget | The bundle stays under the new ceiling | CI |
| Regression rows in the table | No other metric paid for this one | Tech lead |
| Bugbot review of the PR | A second reviewer scans for bugs and security issues in the change | Cursor’s reviewer, then the author |
The author owns the numbers; the tech lead signs off on the table and the green gates and reads code only where the table shows a regression or a test was changed. This is the evidence-over-diffs way of reviewing, applied to performance. For recurring checks, Cursor Automations can start a Cloud Agent from a Sentry or PagerDuty event, so a latency alert arrives with the same baseline-and-rank analysis already attached.
When agent-driven performance work goes wrong
Section titled “When agent-driven performance work goes wrong”The agent optimizes a non-bottleneck. You asked for a faster endpoint and got a 2 ms saving in a function inside a 2,000 ms request. Recovery: return to the ranking prompt and require that every fix names the baseline row it targets. A fix without a row does not get merged.
The numbers in the PR came from the chat, not from a file. The agent “ran a benchmark” in a dev server, or summarized a run that timed out. Recovery: accept only numbers present in perf/after/, produced by the same commands as the baseline, on a production build. The comparison prompt refuses to fill in missing values for this reason.
The faster query returns different data. An inner join drops empty groups, a new cache serves another tenant’s totals, or a rounding change moves cents. Recovery: the response snapshot test fails; revert the handler and ask for the rewrite again with the failing case quoted. Never let the agent update a snapshot in the same change that makes the code faster.
Memoization everywhere. React.memo, useMemo and useCallback cost memory and comparison time. Recovery: remove every memo the profile does not justify, then re-profile; if the interaction time is unchanged, the memos were noise.
A new index slows the write path. Each index adds work to every insert on a high-write table such as usage events. Recovery: before merging, compare insert latency on a write-heavy load run; if it regresses, try a narrower composite index or a pre-aggregated table instead.
Code splitting makes the page slower. A lazily loaded chunk that is small and needed on most pages adds a request for little saving. Recovery: split only chunks that are both large and used on few routes, and check LCP in perf/after/ rather than bundle size alone.
The fast version is unmaintainable. A hand-written query with nested CTEs beats the ORM by 30 ms and nobody can change it. Recovery: keep ORM code by default, drop to raw SQL only for a hot path with a measured requirement, and require a comment that links to the benchmark that justified it.
Where to go next with performance work
Section titled “Where to go next with performance work”The same measure-first loop, written for the other agents: application performance analysis with Claude Code and performance optimization with Codex.