Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Load testing with k6/Artillery, response time thresholds, memory leak detection, N+1 query detection, and CI integration.
.claude/skills/performance-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-12 | ✗→✓ | ▲ Improved | — | — |
| case-09 | ✗→✓ | ▲ Improved | — | — |
javascript// k6 run load-test.js import http from 'k6/http' import { check, sleep } from 'k6' export const options = { stages: [ { duration: '30s', target: 10 }, // ramp up { duration: '1m', target: 50 }, // hold load { duration: '30s', target: 0 }, // ramp down ], thresholds: { http_req_duration: ['p(95)<200', 'p(99)<500'], http_req_failed: ['rate<0.01'], // < 1% error rate }, } export default function () { const res = http.get('https://api.example.com/users') check(res, { 'status is 200': (r) => r.status === 200, 'response time < 200ms': (r) => r.timings.duration < 200, }) sleep(1) }
javascriptexport default function () { const payload = JSON.stringify({ email: 'test@example.com', password: 'secret' }) const headers = { 'Content-Type': 'application/json' } const res = http.post(`${BASE_URL}/auth/login`, payload, { headers }) const token = res.json('token') http.get(`${BASE_URL}/profile`, { headers: { Authorization: `Bearer ${token}` }, }) }
| Type | Duration | Target VU | Purpose | |------|----------|-----------|---------| | Smoke | 1 min | 1-5 | Verify script works, no regressions | | Load | 30 min | expected peak | Normal production conditions | | Stress | 60 min | 2-3x peak | Find breaking point | | Spike | 2 min | 10x peak → 0 | Sudden traffic burst behavior | | Soak | 4-8 hours | 80% peak | Memory leaks, degradation over time |
javascriptexport const options = { thresholds: { // Response time http_req_duration: ['p(95)<200', 'p(99)<500', 'avg<100'], // Error rate http_req_failed: ['rate<0.01'], // < 1% // Custom metric for specific endpoint 'http_req_duration{name:login}': ['p(95)<300'], // Checks pass rate checks: ['rate>0.99'], }, }
yaml# .github/workflows/perf.yml name: Performance Tests on: pull_request: branches: [main] jobs: k6: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run k6 smoke test uses: grafana/k6-action@v0.3.1 with: filename: tests/perf/smoke.js flags: --out json=results.json - name: Upload results uses: actions/upload-artifact@v4 with: name: k6-results path: results.json
bash# Start with --inspect node --inspect --expose-gc server.js # In Chrome DevTools → Memory → Take heap snapshot # Run load, take another snapshot # Compare: growing retained objects = leak
javascriptimport v8 from 'v8' function checkHeap(label) { const stats = v8.getHeapStatistics() console.log(`[${label}] Heap used: ${Math.round(stats.used_heap_size / 1024 / 1024)}MB`) } setInterval(() => checkHeap('monitor'), 30_000)
javascript// BAD: event listener never removed emitter.on('data', handler) // grows on every request // GOOD: cleanup in teardown emitter.on('data', handler) return () => emitter.off('data', handler) // BAD: unbounded cache const cache = {} cache[userId] = data // never evicted // GOOD: bounded cache import LRU from 'lru-cache' const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 })
sql-- Enable extension CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- Find repetitive queries during a load test window SELECT query, calls, mean_exec_time, total_exec_time FROM pg_stat_statements WHERE calls > 100 ORDER BY calls DESC LIMIT 20;
javascript// Prisma: log all queries const prisma = new PrismaClient({ log: ['query'], }) // Detect N+1: same query fired N times in a request // Fix: use include/select or DataLoader
javascriptimport DataLoader from 'dataloader' const userLoader = new DataLoader(async (ids) => { const users = await db.user.findMany({ where: { id: { in: ids } } }) return ids.map(id => users.find(u => u.id === id)) }) // In resolver — batches automatically const user = await userLoader.load(post.authorId)
yaml# .github/workflows/lhci.yml - name: Lighthouse CI run: | npm install -g @lhci/cli lhci autorun env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
json// lighthouserc.json { "ci": { "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.8 }], "first-contentful-paint": ["error", { "maxNumericValue": 2000 }], "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }], "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }] } } } }
Store k6 results to Grafana/InfluxDB for trend visualization:
bashk6 run --out influxdb=http://localhost:8086/k6 load-test.js
Or export JSON and compare baselines:
bashk6 run --out json=results-$(git rev-parse --short HEAD).json load-test.js
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-24 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +21 percentage points is the difference between those two pass rates over the 24 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.