Performance
Why your NestJS API is slow
Two numbers explain most of it: how many queries one request ran, and how long the slowest took. Sixty small queries is a structure problem, not an index one.
"The site is slow" is a complaint, not a diagnosis. The first thing to establish is where the time goes: before the first byte leaves the server, or after it, in the browser. These are two different measurements with two different cures, and most of the effort spent on site speed is spent treating the wrong end.
curl -o /dev/null -s -w 'dns:%{time_namelookup} connect:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' https://example.com/The number that matters here is time_starttransfer — time to first byte. Under 300 ms and your problem is on the front end: heavy images, render-blocking CSS, fonts pulled from a third domain. A second or more and the problem is inside PHP and MySQL, and no amount of image compression will touch it.
Repeat the measurement on the homepage, a post, an archive, and once while logged in. The spread between those four tells you a lot. Page caches serve anonymous visitors and not logged-in users, so if the page is fast for a stranger and slow for you, what you are seeing is the real generation time with the cache pulled away.
Install Query Monitor on a staging copy, or enable it for a single admin user. It is the only tool that ties a slow query back to the plugin that fired it, and that link is the whole diagnosis. Three numbers matter: query count, total query time, and the single slowest query. A healthy WordPress page does its work in a few dozen queries. Four hundred queries on one page means a plugin is calling get_post_meta inside a loop with nothing primed.
A bloated wp_options. Every page request loads all autoloaded rows before any useful work begins:
SELECT SUM(LENGTH(option_value)) AS bytes
FROM wp_options WHERE autoload IN ('yes','on');Healthy is under a hundred kilobytes. Sites exist that load eight megabytes on every request because an abandoned plugin wrote its logs into an autoloaded option. Name the largest rows before deleting anything, and count expired transients while you are there:
SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_options WHERE autoload IN ('yes','on')
ORDER BY bytes DESC LIMIT 20;meta_query lookups. wp_postmeta is indexed on meta_key, not on meta_value. Any filter or sort on the value is a full table scan, and it degrades linearly as content grows. WooCommerce stores hit this first. The fix is usually not a new index but moving the field you filter on into a taxonomy or a table of its own.
wp-cron on every request. By default WordPress checks its schedule on each visit, which means one heavy job is paid for by a random visitor. Turn it off and run a real system cron instead:
// wp-config.php
define('DISABLE_WP_CRON', true);*/5 * * * * cd /home/user/public_html && wp cron event run --due-nowadmin-ajax.php and the heartbeat. If only the dashboard is slow, open the network tab and look at what repeats. Plenty of plugins poll the server every fifteen seconds for no reason anyone can defend.
External HTTP inside the request path. A plugin checking its licence, fetching tweets, or asking an ad server — all of it happening between the click and the page. Query Monitor lists those in their own panel. Every synchronous outbound request is both a delay and a point of failure.
The order is deliberate. A cache hides the problem rather than solving it, and a slow site with a cache is still slow for the first visitor after every update, for every logged-in user, and for every page not yet warmed. Once the above is fixed, add caching in this order:
Re-run the same curl after each change, not after all of them. A change that does not move the number was a change you did not need, and now you know that instead of assuming it. One caution: do not load-test on shared hosting. Tools like ab and hey saturate the CPU core allocated to your account, so you end up measuring the account limit rather than the site, and annoying your neighbours on the server for nothing.
Performance
Two numbers explain most of it: how many queries one request ran, and how long the slowest took. Sixty small queries is a structure problem, not an index one.
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.