With the application complete and tested, applying the performance optimisation techniques from Chapter 22 reveals how the patterns studied throughout the course manifest in a real application. This lesson profiles the Task Manager, identifies the actual bottlenecks (not the assumed ones), applies MongoDB index optimisation, adds Redis caching where it is missing, tunes Angular change detection, and sets up the Prometheus metrics dashboard that monitors the application in production. The lesson demonstrates the measure-first, optimise-second discipline.
Performance Audit Results (Before Optimisation)
| Endpoint / Operation | Before | Bottleneck | Fix |
|---|---|---|---|
| GET /tasks?workspaceId= | 280ms | COLLSCAN on workspace field | Add compound index { workspace:1, status:1, createdAt:-1 } |
| GET /workspace-dashboard | 450ms | 4 sequential queries | Replace with single $facet aggregation |
| Task list Angular render | 120ms jank | No trackBy, Default change detection |
Add trackBy: task._id, OnPush |
| POST /auth/login | 340ms | No index on email field | Add { email: 1 } unique index (was already there — found missing in test DB) |
| Notification bell badge | 85ms | COUNT query on every page load | Read denormalised user.unreadNotifications field instead |
db.setProfilingLevel(1, { slowms: 50 }) in the MongoDB shell against your staging database while running a realistic load test (k6 with 50 concurrent users, 2-minute run). Then query db.system.profile.find().sort({ millis: -1 }).limit(20) to see the top 20 slowest operations. This is almost always more informative than guessing which queries are slow — production query patterns often differ significantly from what developers expect based on their mental model of the application.Applied Performance Optimisations
// ── 1. Missing indexes discovered via profiling ────────────────────────────
// Run after db.setProfilingLevel(1, { slowms: 50 }) + load test:
// db.system.profile.find({ millis: { $gt: 50 } }).sort({ millis: -1 }).limit(10)
//
// Found: tasks queries using COLLSCAN on { workspace, status }
// Fix: add to task.model.js (already there — verify it exists in production):
// taskSchema.index({ workspace: 1, status: 1, createdAt: -1 });
// ── 2. Dashboard: 4 queries → 1 $facet aggregation ───────────────────────
// BEFORE: 4 sequential queries averaging 450ms total
async function getDashboardSlow(workspaceId) {
const total = await Task.countDocuments({ workspace: workspaceId, deletedAt: { $exists: false } });
const done = await Task.countDocuments({ workspace: workspaceId, status: 'done' });
const overdue = await Task.countDocuments({ workspace: workspaceId, dueDate: { $lt: new Date() }, status: { $ne: 'done' } });
const byStatus = await Task.aggregate([{ $match: { workspace: workspaceId } }, { $group: { _id: '$status', count: { $sum: 1 } } }]);
return { total, done, overdue, byStatus };
}
// AFTER: single $facet — ~95ms
async function getDashboard(workspaceId) {
const wsId = new mongoose.Types.ObjectId(workspaceId);
const [result] = await Task.aggregate([
{ $match: { workspace: wsId, deletedAt: { $exists: false } } },
{ $facet: {
totals: [
{ $group: {
_id: null,
total: { $sum: 1 },
done: { $sum: { $cond: [{ $eq: ['$status','done'] }, 1, 0] } },
overdue: { $sum: { $cond: [
{ $and: [{ $lt: ['$dueDate', new Date()] }, { $ne: ['$status','done'] }] },
1, 0
]}},
dueThisWeek: { $sum: { $cond: [
{ $and: [
{ $gte: ['$dueDate', new Date()] },
{ $lte: ['$dueDate', new Date(Date.now() + 7*86400000)] },
]}, 1, 0
]}},
}},
],
byStatus: [
{ $group: { _id: '$status', count: { $sum: 1 } } },
],
completionTrend: [
{ $match: { status: 'done', completedAt: { $gte: new Date(Date.now() - 30*86400000) } } },
{ $group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$completedAt' } },
count: { $sum: 1 },
}},
{ $sort: { _id: 1 } },
],
tagCloud: [
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 20 },
],
}},
]);
return {
...result.totals[0],
byStatus: result.byStatus,
completionTrend: result.completionTrend,
tagCloud: result.tagCloud,
};
}
// ── 3. k6 load test — identify regressions ───────────────────────────────
// k6-scripts/task-list.js (run: k6 run --vus 50 --duration 2m script.js)
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';
const taskListLatency = new Trend('task_list_latency');
export const options = {
thresholds: {
http_req_duration: ['p(95)<300'], // 95th percentile under 300ms
http_req_failed: ['rate<0.01'], // <1% errors
},
};
export function setup() {
const res = http.post(`${__ENV.API_URL}/auth/login`,
JSON.stringify({ email: 'owner@test.io', password: 'Password123!' }),
{ headers: { 'Content-Type': 'application/json' } }
);
return { token: res.json('data.accessToken'), workspaceId: res.json('data.user.defaultWorkspaceId') };
}
export default function ({ token, workspaceId }) {
const start = Date.now();
const res = http.get(`${__ENV.API_URL}/tasks?workspaceId=${workspaceId}&page=1&limit=20`, {
headers: { Authorization: `Bearer ${token}` },
});
taskListLatency.add(Date.now() - start);
check(res, { 'status 200': r => r.status === 200, 'has data': r => r.json('data') !== null });
sleep(0.5);
}
// ── 4. Angular: pure pipe replaces method call in task-card ───────────────
// BEFORE: method called on every change detection cycle
// template: {{ formatDueDate(task.dueDate) }}
// component: formatDueDate(date: string) { return ... }
// AFTER: pure pipe — cached per input value
@Pipe({ name: 'relativeDate', standalone: true, pure: true })
export class RelativeDatePipe implements PipeTransform {
transform(date: string | null): string {
if (!date) return '';
const d = new Date(date);
const now = new Date();
const diffMs = now.getTime() - d.getTime();
const diffDays = Math.floor(diffMs / 86400000);
if (diffDays === 0) return 'today';
if (diffDays === 1) return 'yesterday';
if (diffDays < 7) return `${diffDays}d ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
return d.toLocaleDateString();
}
}
// ── 5. Prometheus metrics — task-specific business metrics ────────────────
const { tasksCreated, tasksCompleted } = require('./config/metrics');
// In task.controller.js — after successful create:
tasksCreated.inc({ priority: task.priority, workspace: task.workspace.toString() });
// In task.controller.js — after status changes to 'done':
tasksCompleted.inc({ workspace: task.workspace.toString() });
// Grafana dashboard query examples:
// Task creation rate: rate(taskmanager_tasks_created_total[5m])
// Completion rate: rate(taskmanager_tasks_completed_total[5m])
// Creation by priority: sum by (priority)(rate(taskmanager_tasks_created_total[5m]))
How It Works
Step 1 — Profiling Reveals Real Bottlenecks
The slow query profiler running during a k6 load test produced a list of actual slow operations. The dashboard’s 4-query problem was the biggest win — consolidating into one $facet aggregation dropped latency from 450ms to 95ms. The pure pipe fix eliminated hundreds of function calls per second during task list scrolling. Neither of these improvements would have been prioritised based on intuition alone — profiling provided the evidence.
Step 2 — $facet Aggregation Consolidates Dashboard Queries
The dashboard refactoring shows a 4.7x latency improvement (450ms → 95ms) by replacing 4 sequential queries with 1 $facet aggregation. The aggregation also reads the collection once (after the initial $match) rather than four times, reducing I/O proportionally. The business logic moved from application code (4 separate async operations) to the database (1 compound aggregation) — where it executes closer to the data.
Step 3 — k6 Thresholds Create Performance Regression Tests
The k6 script with thresholds: { http_req_duration: ['p(95)<300'] } acts as a performance regression test in CI. If a code change degrades the p95 task list latency above 300ms, the k6 test exits with code 1 and the CI build fails. Running this against the staging environment after every deployment catches performance regressions before they reach production users.
Step 4 — Pure Pipe Eliminates Template Method Calls
The formatDueDate method called in every task card’s template ran on every change detection cycle — with 20 tasks on screen, that is 20 method calls per cycle, many cycles per second. The pure pipe is called only when the input value changes. Over a 5-minute task list session with typical user interactions, this eliminates thousands of redundant date format computations. The improvement is measurable via Angular DevTools profiler: component check time drops from 8ms to 0.4ms.
Step 5 — Business Metrics Connect Performance to Outcomes
Adding tasksCreated.inc() and tasksCompleted.inc() counters allows Grafana dashboards to show business health alongside technical health. A deployment that causes a drop in task creation rate (users encountering errors or slowness they don’t report) is visible immediately in the metrics — even before support tickets arrive. Correlating a latency spike with a drop in task creation rate proves user impact, which drives prioritisation.
Quick Reference
| Optimisation | Technique | Improvement |
|---|---|---|
| Enable slow query log | db.setProfilingLevel(1, { slowms: 50 }) |
Reveals real bottlenecks |
| Multi-query → $facet | Replace N sequential aggregations with one $facet | 4.7x latency reduction |
| Template method → pipe | @Pipe({ pure: true }) |
Eliminates per-cycle calls |
| Load test threshold | k6 thresholds: { p(95)<300 } |
Automatic regression detection |
| Business metric | tasksCreated.inc({ priority }) |
User impact visibility |
| Read slow query log | db.system.profile.find({ millis: { $gt: 50 } }).sort({ millis: -1 }) |
Top slowest operations |