Performance
Why a WordPress site gets slow, and how to actually diagnose it
Time to first byte separates a server problem from a browser one. Start there, chase the queries and the options table, and add caching last.
An endpoint that takes 900ms is not a mystery, but it is unreadable from the outside. Before changing anything, make the request tell you where its time went. Turn on query logging in the ORM and count the statements for one request:
// TypeORM
new DataSource({logging: ['query'], maxQueryExecutionTime: 100});Two numbers decide almost every case. How many queries did one request run, and how long did the slowest one take? A single 400ms query is an index problem. Sixty queries of 4ms each is a structure problem, and it is far more common.
You fetch fifty orders, then read order.customer in a loop, and the ORM issues fifty more queries without telling you. It looks like property access. It is network round trips.
// one query per order, silently
const orders = await repo.find();
for (const o of orders) console.log(o.customer.name);
// one query, joined
const orders = await repo.find({relations: {customer: true}});Lazy relations are the usual cause, and the reason they hurt is that they make the expensive thing look free. If you use GraphQL, the same problem arrives per-field across resolvers and the fix is a DataLoader that batches within a tick rather than eager relations.
Read the query count for your three slowest endpoints. If it scales with the number of rows returned, you have found the bug and everything below is secondary.
Every request holds a database connection while it works. The pool has a size, usually about ten, and when all of them are busy the next request waits — not slowly, just waits, and the latency you measure is queueing rather than work.
Raising the pool size is the wrong first move; it usually just moves the contention into the database. Shorten how long each request holds a connection instead: no HTTP calls inside a transaction, no await on something unrelated between opening and committing, and never a transaction spanning a request you do not control.
ValidationPipe with transform on a large payload, or a logging interceptor that serialises the whole response body, is a cost you pay per request and never see in a query log.class-transformer is not free. plainToInstance over a few thousand rows is real CPU. Return the shape you need from the query instead of fetching entities and mapping them.await in a loop is a sequence. Promise.all over independent work is usually a one-line change with a large effect.Caching is the step after the structure is right, because a cache in front of an N+1 hides it, and it will come back as a stampede the first time the cache expires under load.
When you do add one, cache the expensive read rather than the whole response, give it an explicit TTL, and decide what invalidates it before you ship it. A cache with no invalidation story is a bug with a delay on it.
After each change, measure the same endpoint the same way — concurrency you actually expect, and the 95th percentile rather than the average. Averages hide the requests that lose you customers, and the reason to fix the N+1 first is that it is the one that gets dramatically worse exactly when you are busiest.
Performance
Time to first byte separates a server problem from a browser one. Start there, chase the queries and the options table, and add caching last.
Security
Four NestJS advisories in seven months, all one bug: the matcher and the handler disagreed about the request. The check belongs where the data is read.
SEO
Next.js merges metadata key by key, so a route that omits alternates inherits its layout canonical. Ours pointed every page at the homepage.