"It feels slow" is not a diagnosis. Before you optimize anything, you need a real measurement of where time and memory actually go during a request — otherwise you risk spending an afternoon speeding up a function that was never the bottleneck in the first place. Profiling turns a vague feeling into a concrete, ranked list of exactly what's expensive.
What you'll learn in this lesson
- How to install and read output from Webprofiler, the standard Drupal profiling tool
- What the profiler's timeline actually shows you, request by request
- A lightweight manual technique for timing a specific suspect function without any tooling at all
- The difference between profiling in a browser and profiling from the CLI
Installing Webprofiler
composer require --dev drupal/devel
ddev drush en devel webprofiler -y
Webprofiler ships as part of the Devel project. Once enabled, a small toolbar appears fixed to the bottom of every page, showing at-a-glance numbers — total request time, memory used, number of database queries — for the page you're currently looking at. Click through it for the full breakdown.
Reading the timeline
The detailed profiler view breaks a single request into phases: kernel boot, routing, controller execution, view/render, and response. For a genuinely slow page, this immediately tells you which phase to investigate — a slow "controller" phase points at your own code's logic; a slow "render" phase points at expensive theme/template work; a large query count points at the database, not PHP at all.
- Database tab — every query the request ran, with its exact SQL and execution time. A repeated identical query (the classic N+1 pattern — one query per item in a loop instead of one query for all of them) shows up immediately as the same SQL string appearing dozens of times.
- Services tab — which services got instantiated for this request and how long each took to build.
- Cache tab — cache hits vs. misses for this specific request, useful for confirming your
#cachemetadata is actually working the way you think it is.
Manual timing, when you don't need the full toolchain
$start = microtime(TRUE);
$result = $this->expensiveOperation($data);
$elapsed_ms = (microtime(TRUE) - $start) * 1000;
\Drupal::logger('my_module')->debug('expensiveOperation() took @ms ms.', ['@ms' => round($elapsed_ms, 2)]);
For a quick, targeted check — "is this one specific function the actual problem?" — a manual microtime(TRUE) measurement logged around just that call is often faster to set up than reaching for a full profiler, and it works identically in a CLI script (a cron job, a drush command) where a browser-based toolbar wouldn't even load.
Quick check: Webprofiler's Database tab shows the exact same SELECT query, with different WHERE values, running 200 times in one request. What does that pattern usually indicate, and what's the general fix? (Answer: an N+1 query problem — a query running once per item inside a loop. The general fix is to replace it with a single batched query, e.g. loading all the needed entities at once with an
INcondition instead of oneload()call per iteration.)
Key takeaways
- Never optimize based on a feeling — measure first with Webprofiler or a manual
microtime()check, then fix what the numbers actually show. - Webprofiler's Database tab is usually where the biggest, easiest wins hide — especially repeated identical queries (N+1 patterns).
- A manual
microtime(TRUE)measurement around a specific suspect function is faster to set up than a full profiler for a narrow, targeted question. - Profile against realistic data volumes — a fast local measurement on a tiny dataset can hide a real production-scale problem.
- Webprofiler is a dev-only tool, same as the rest of Devel — never leave it enabled on production.
Coming up next
Your module now works, is translatable, and you know how to debug and profile it. The last stretch of this tutorial is about getting it ready for other people to actually use — starting with what a real module needs before it's ready to publish on Drupal.org.