Using profiling tools to identify slow componentsfor Drupal 8 , 9 , 10 , and 11

Last updated :  

You've measured that a page is slow (load testing) and you're watching it continuously in production (monitoring). This lesson closes the loop: profiling — actually opening up a single slow request and seeing, function by function, exactly where the time went. If monitoring is a dashboard showing you a fever, profiling is the blood test that tells you which organ is actually the problem.

What you'll learn in this lesson

  • The difference between a profiler and an APM trace, and when each is the right tool
  • How to use Xdebug's built-in profiler locally, and read its output with a flame graph tool
  • How to use Drupal core's own Devel module for quick, low-ceremony profiling during development
  • What to actually do once you've found the bottleneck function

Profiler versus APM trace

An APM trace (from the previous lesson) is designed to run continuously in production with minimal overhead — it samples and summarizes. A profiler is the opposite: it runs once, on a single request you deliberately trigger, and records everything, at a level of detail far too expensive to leave running on live traffic. You reach for a profiler when you already know roughly which page is slow (from monitoring or a bug report) and need to go from "this page is slow" to "this specific function call, called this many times, is why."

Xdebug's profiler

; php.ini or a DDEV .ddev/php/xdebug.ini override
xdebug.mode = profile
xdebug.output_dir = /tmp/xdebug_profiles
xdebug.start_with_request = trigger

With start_with_request = trigger, Xdebug only profiles a request when you explicitly ask it to — append ?XDEBUG_PROFILE=1 to a URL, or set the equivalent cookie via a browser extension. This matters: profiling every request would slow your dev environment to a crawl, since Xdebug's profiler records every single function call and its exact timing.

The output is a cachegrind.out.* file — not human-readable directly. Open it in a tool built for Cachegrind format, like KCachegrind (Linux/Windows), QCachegrind (macOS), or the profiling view built into most PHP-aware IDEs, to get a flame graph or call-tree view: the width of each bar is time spent, and you can immediately see which function dominates.

Devel module's quick profiling

Drupal core's companion Devel module (contrib, essential for local dev, never enable it in production) ships lighter-weight tools for the same job without leaving your browser: a query log showing every database query on the current page with its execution time, and a "Execute PHP" facility for quick ad-hoc timing checks. For a quick "which of these three queries is slow" question, Devel's query log is often faster to reach for than a full Xdebug profile.

// Devel's dpm() (debug print message) is a fast way to inspect a value's
// shape while profiling, without a full var_dump() dump cluttering output.
dpm($entity->id(), 'Entity ID being loaded');

// Devel's own Timer utility for quick manual timing around a suspect block.
\Drupal\Component\Utility\Timer::start('my_suspect_block');
// ... code you suspect is slow ...
$elapsed = \Drupal\Component\Utility\Timer::stop('my_suspect_block')['time'];
\Drupal::logger('mymodule')->debug('Block took @ms ms', ['@ms' => $elapsed]);
Never enable Xdebug or Devel's verbose logging in production. Both add real overhead per request, and Devel's debug output functions can leak sensitive data into logs or the page itself if accidentally left active. Profile locally or on a dedicated staging copy of production data, not the live site.

What to do once you've found the bottleneck

A profile pointing at a single function only tells you where — the fix still depends on why. A function dominated by database time usually means a missing index or an N+1 query pattern (one query per item in a loop, instead of one query for all items). A function dominated by pure PHP time with no I/O usually means an algorithmic problem — nested loops over large arrays, or repeated work that could be cached or computed once. The profile narrows the search; understanding the code is still the actual fix.

See it for yourself

On your own DDEV site, enable Xdebug (ddev xdebug on) and profile a page you know does real work — visit it with ?XDEBUG_PROFILE=1 appended, then find the resulting cachegrind.out.* file under your project's /tmp mount and open it in a Cachegrind viewer.

Quick check: your profile shows one function called 500 times on a single page load, each call taking only 2ms — versus a different function called once, taking 400ms. Which is the better target to optimize first, and why might the 500-call function actually be the more valuable fix even though each individual call is fast? (500 × 2ms = 1000ms total, more than the single 400ms call — and a function called 500 times on one page is a strong signal of an N+1 query pattern, which usually has a clean, well-understood fix: eager-load the data once instead of once per item.)

Key takeaways

  • Use a profiler (Xdebug) for deep, one-off investigation of a known-slow request; use APM for continuous production monitoring — they're complementary, not interchangeable.
  • Xdebug's start_with_request = trigger mode profiles only requests you explicitly flag, keeping your dev environment usable the rest of the time.
  • Devel module's query log and Timer utility are faster for quick "which query is slow" questions than a full profile.
  • Never run Xdebug's profiler or Devel's verbose debugging in production — both add real overhead and potential data leakage.
  • A high call-count function with a low per-call cost can matter more than a single slow call — total time, not per-call time, is what to optimize.

Coming up next

You've now covered the full performance-optimization arc for the site's server side — caching, database, frontend assets, infrastructure, and the diagnostic tools to find what's actually slow. Next, the focus shifts specifically to headless and API-driven Drupal, where JSON:API performance becomes the thing your frontend's entire perceived speed depends on.