In this article, we will explore how to use Xdebug Profiler to analyze and optimize PHP performance in Magento 2. You will learn how to identify slow code, interpret profiling data, and apply practical optimizations.
We will go through setting up the profiler, generating a performance snapshot, and comparing a deliberately slow implementation with its optimized counterpart to illustrate how small code changes can dramatically improve execution time.
What Is Xdebug Profiler?
Xdebug Profiler is a performance analysis tool built into the Xdebug extension for PHP.
It records detailed information about what happens inside your PHP code during execution, including:
- how many times each function is called
- how much time each function takes
- average time per call
- call stack depth
- CPU usage per function
In other words, the profiler provides a precise breakdown of where the application spends time.
Instead of guessing which part of the system is slow, you get an exact, data-driven map of the execution flow and bottlenecks.
Why Do You Need a Profiler?
Modern PHP frameworks are large, highly dynamic systems with many layers working together. A typical request may involve:
- dependency injection containers
- plugins, middleware, observers, interceptors
- ORM or EAV-style data models
- complex business logic and indexing processes
- multiple caching layers: page cache, application cache, database cache, Redis/Valkey
Because so many components participate in every request, it is often unclear which exact part of the system is responsible for performance degradation.
On platforms such as Magento, this effect becomes even more pronounced because of the heavy use of EAV models, dynamic configuration, and layered caching. As a result, a real profiler becomes indispensable for accurate performance analysis.
To solve this, Xdebug Profiler provides a clear visual breakdown of where time is actually spent:
Requests in modern PHP frameworks involve many DI objects, plugins, middleware, and caching layers. The profiler shows which functions consume the most time and how much CPU each part uses.
Loops, repeated SQL queries, large string operations and unnecessary processing become visible right away. This helps identify wasted work that slows down execution.
EAV-based collections often load more attributes than needed. The profiler highlights heavy SQL queries and shows when attribute loading becomes expensive.
Useful for optimization. You can profile the initial version, apply improvements, and then profile again. Clear metrics make it easy to confirm whether performance improved.
Instead of relying on subjective impressions like “feels faster”, you get measurable data:
- call counts
- execution time
- CPU cost
- flamegraphs
- call trees
Why Xdebug Profiler Is Especially Useful for PHP Developers
Modern PHP applications are often built on top of complex frameworks and libraries. This architectural flexibility significantly improves development speed and extensibility, but it also increases execution cost. Even relatively simple PHP code can indirectly trigger:
- dozens of dependency injection calls
- multiple plugin or middleware chains
- heavy ORM or EAV-style data loading
- large and complex SQL queries
- deeply nested loops inside framework internals
In such environments, it is rarely obvious where the real performance bottleneck is located.
Xdebug Profiler helps PHP developers:
- detect slow functions, blocks, or templates
- analyze expensive data collections and queries
- identify overhead caused by DI containers, plugins, and interceptors
- locate unnecessary data loading
- refactor CPU-intensive loops
- objectively validate performance improvements before production
Although the practical examples in this article are demonstrated using Magento, all profiling principles and optimization techniques shown here apply equally to any PHP-based application or framework.
For this reason, Xdebug Profiler is an essential tool for anyone working on high-performance PHP systems.
Setting Up Xdebug Profiler
Xdebug includes a built-in profiler that records detailed information about how PHP code is executed. The profiler generates cachegrind files that can be visualized using various tools.
In this article, we will use PHPStorm’s built-in Profiler viewer, which provides an excellent UI for analyzing PHP and Magento performance issues.
However, alternative tools such as QCacheGrind / KCacheGrind, and WebGrind are also available and work with the same output format.
Below is a clean and reliable configuration for enabling the profiler in a Magento 2 development environment.
1. Enable Xdebug Profiler
Open your 15-xdebug.ini (typically located in /etc/php.d/ or inside your Docker container) and configure the following:
zend_extension=xdebug.so
xdebug.mode=profile
xdebug.start_with_request=trigger
xdebug.output_dir="/tmp/xdebug"
xdebug.profiler_output_name="cachegrind.out.%p"
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.discover_client_host=1
xdebug.idekey=PHPSTORM
xdebug.trigger_value=startProfile
What these settings mean
| Setting | Explanation |
|---|---|
xdebug.mode=profile | Enables only the profiler, without debugging or tracing. |
xdebug.start_with_request=trigger | Profiler starts only when triggered via GET/POST/COOKIE. |
xdebug.output_dir | Where profiling files will be saved. Must be writable by PHP. |
xdebug.profiler_output_name | Filename pattern (process ID in this case). |
xdebug.trigger_value=startProfile | Name of the trigger you will use. |
Using triggers is important – this prevents Xdebug from profiling every Magento page load, which would quickly flood your disk with hundreds of megabytes of data.
2. Create the Output Directory
Inside your Magento installation, create the profiler output folder:
mkdir -p /tmp/xdebug
chmod 777 /tmp/xdebug
Xdebug must have permission to write inside this folder.
3. Restart PHP-FPM or the Docker Container
Native Linux / macOS (for PHP 8.2):
sudo systemctl restart php8.2-fpm
Docker:
docker restart <php-container>
Magento Cloud:
php-fpm:restart
Xdebug will not apply changes until PHP is restarted.
4. Trigger the Profiler in Your Browser
Because we enabled start_with_request=trigger, the profiler activates only when you pass the trigger value. For example:
https://test-domain.local/test?XDEBUG_TRIGGER=startProfile
After loading the page, you should see new files in the profiler directory:
cachegrind.out.89.gz
cachegrind.out.668.gz
Each file represents one profiled request.
4.1 Triggering Xdebug Profiler via Cookie for AJAX Requests
Besides URL parameters, Xdebug Profiler can be activated using browser cookies. This is especially useful for profiling specific AJAX requests without affecting full page loads.
When xdebug.start_with_request=trigger is enabled, Xdebug checks the XDEBUG_TRIGGER value in cookies. By limiting the cookie Path, you can profile only selected endpoints for a limited time.
Examples for Magento AJAX requests with a 120-second profiling window:
Add to cart request:
XDEBUG_TRIGGER=startProfile; Path=/checkout/line-item/add; Max-Age=120; SameSite=Lax
Offcanvas cart update:
XDEBUG_TRIGGER=startProfile; Path=/checkout/offcanvas; Max-Age=120; SameSite=Lax
Checkout info widget:
XDEBUG_TRIGGER=startProfile; Path=/widgets/checkout/info; Max-Age=120; SameSite=Lax
Cart page request:
XDEBUG_TRIGGER=startProfile; Path=/checkout/cart; Max-Age=120; SameSite=Lax
Profiling stops automatically after the specified time or immediately when the cookie is removed by setting Max-Age=0.
This method allows you to capture performance data only for the exact AJAX requests you need to analyze.
5. Analyzing Profiler Files in PHPStorm
PHPStorm provides a built-in profiler viewer that supports Cachegrind format:
- Flamegraph
- Call Tree
- Execution Statistics
- Function Costs
- Time spent per file, per class, per method
To launch a PHPStorm profiler file: Select Tools ➤ Analyze Xdebug Profiler Snapshot from the main menu.
In the Select Xdebug profiler snapshot dialog that opens, choose the folder and the file where the profiling data is stored.
Practical Example: Testing a Slow Page with Xdebug Profiler
Step 1: Creating a Slow Test Page
For this example, we created a dedicated Magento test page that intentionally loads very slowly.
To ensure clean measurement conditions, Full Page Cache (FPC) was disabled, so the block executes on every request.
This allows us to test performance exactly as Magento processes the page internally, without caching interference.

Step 2: Running the Profiler on the Page
To generate a profiler snapshot, open the test page with an Xdebug trigger:
https://test-domain.local/test?XDEBUG_TRIGGER=profile
Once the page finishes loading, Xdebug generates a cachegrind.out.89.gz file.
Finding the Profiler File Name in Browser DevTools (optional but useful)
Before opening PHPStorm, you can confirm which profiler file was generated by inspecting response headers in the browser.
In Chrome DevTools:
- Open Network ➤ [your request] ➤ Response Headers
- Look for the header:
X-Xdebug-Profiler-Filename
This header contains:
- the exact filename of the generated profiler snapshot;
- the full absolute path to the file on disk.
Example:
X-Xdebug-Profiler-Filename: /tmp/xdebug/cachegrind.out.87.gz

This helps verify that:
- the profiler triggered correctly,
- the file was successfully created,
- you know exactly where it is located.
Step 3: Analyzing the Profiling Results
After opening the profiler snapshot in PHPStorm (see previous section), we immediately get a detailed list of all executed functions, sorted by execution time.
To understand which parts of our custom test page cause the slowdown, we focus on the Execution Statistics tab.
1. Reviewing the Execution Statistics Table
This table shows:
- total time,
- own time,
- number of calls,
- memory usage for each function.
It is immediately apparent that most of the time is spent inside our custom block.

What we see:
Vendor\Module\Block\Page->getSaleStats()takes 87% of total request time.- Inside it,
heavyCpuWork()alone consumes 36%. getProducts()is also heavy, contributing 17%.
This immediately proves that the performance issue is not Magento core, but our own custom logic.
2. Inspecting the Call Tree
The Call Tree view provides a detailed breakdown of how execution flows through the request. By expanding the stack under our custom template, we can clearly see which methods are called in sequence and how deeply they are nested. This makes it easy to spot the slow sections of the chain and understand why the block takes so long to execute.

Observations:
getSaleStats()callsheavyCpuWork()thousands of times.- Each product triggers expensive attribute lookups (
getData,getId,getPrice). - The method chain is inefficient by design – this matches the intentional “slow” test version.
This confirms that the performance problem is structural, not incidental.
3. Verifying Slowest Functions via Own Time
Sorting by Own Time is the most accurate way to find the real bottleneck: it excludes time spent in child calls.

heavyCpuWork()dominates Own Time – meaning it is the single most expensive function.- The custom block consumes most execution time overall.
- Magento internal functions appear far below – so the slowdown is entirely our responsibility.
Conclusion of the Analysis Stage
From the profiler data, we now have a clear and evidence-based understanding:
- Our test page is slow because of two custom methods:
- The excessive CPU function (
heavyCpuWork) - The inefficient product loading
- The excessive CPU function (
These functions are the primary targets for optimization.
Step 4: Analyzing and Optimizing the Code
Based on the profiler results from the previous section (see “Analyzing the Profiling Results”), it is clear which part of the system is responsible for the slowdown: our custom block Vendor\Module\Block\Page.
The profiler tables and call tree both show significant execution time inside:
Vendor\Module\Block\Page->getSaleStats()Vendor\Module\Block\Page->heavyCpuWork()- and additional overhead in
getProducts()due to loading all product attributes.
The profiled implementation loads the full product collection with all attributes, iterates over every product in PHP, performs additional CPU-intensive operations inside the loop, and applies sale filtering after the data is already loaded into memory.
Functionally, the block calculates the number of products on sale and the average discount. From a performance perspective, the main issues are excessive data loading, heavy processing inside a tight loop, and late filtering at the PHP level instead of the database.
In the next section, we will review how this logic was optimized.
Optimized Code and What Changed
Based on the profiler results from the previous step (see the analysis section above), it became clear that the main bottleneck was inside our custom block class Vendor\Module\Block\Page. The optimized version keeps the same public API and business result, but removes unnecessary work and reduces the amount of data processed per request.
Below are the key changes shown as compact before and after fragments.
1. Removed artificial CPU load from the loop
In the slow version, each product iteration called heavyCpuWork() with thousands of md5() operations. This method did not affect the result at all and only consumed CPU time.
In the optimized version:
heavyCpuWork()has been completely removed.- The loop in
getSaleStats()now performs only the work required for the business logic:- check
isOnSale(), - calculate discount,
- update counters.
- check
Effect in profiler:
heavyCpuWork()andmd5()disappear from the top rows in the statistics table.- Own Time for our block methods drops significantly.
- The call tree becomes much smaller and easier to read.
This is a good illustration of a typical real-world situation: legacy or temporary “debug” code remains in production and silently slows down page rendering until profiling reveals it.
2. Slimmer product collection: only required attributes
Originally, the collection loaded all EAV attributes for every product, even though the block used only a few of them:
$collection->addAttributeToSelect('*');
This forces Magento to load the full EAV attribute set for each product, even though the block needs only price and special price dates.
In the optimized version we explicitly select only the attributes used in the block:
$collection->addAttributeToSelect([
'price',
'special_price',
'special_from_date',
'special_to_date',
]);
Effect in profiler:
- Time spent in EAV loading methods is reduced.
- The SQL query becomes lighter (fewer columns), which reduces I/O and memory usage.
- The block still has all the data it needs for the calculation, but no more than that.
3. Filtering products on the database side
In the initial version, every product in the store was loaded and passed through isOnSale(). Products without special_price or with invalid values were filtered only in PHP.
The optimized version adds a simple but effective collection filter:
$collection->addAttributeToFilter('special_price', ['gt' => 0]);
Now:
- the database returns only products that have a
special_price > 0; isOnSale()still validates dates and comparespecial_pricetoprice, so the business logic remains correct and safe;- the number of iterations in the PHP loop decreases, especially on large catalogs.
Effect in profiler:
- The total number of calls to
isOnSale()and related methods drops. - Overall time in
getSaleStats()decreases, even though the logic stays the same.
4. What did not change
A key point of this refactoring is:
- The public method
getSaleStats()keeps the same signature and return format. - The business result stays the same:
on_sale_count: number of products on sale;average_discount:calculated the same way as before.
We changed how the data is fetched and processed, not what the method returns.
This is important from a testing and Magento integration perspective:
the template, layout, and any other code using this block do not require changes.
Step 5: Profiling the Optimized Code
After updating the block to the optimized version (see Optimized Code and What Changed), we repeat the same profiling procedure to verify the result in numbers.
1. Run the profiler again with the updated code
We use exactly the same URL and trigger as in the first test:
https://test-domain.local/test?XDEBUG_TRIGGER=profile
This is important: the request must be identical to the previous run so that the results are comparable.

Once the page finishes loading, Xdebug generates a new profiler file in the same directory as before, for example:/tmp/xdebug/cachegrind.out.686.gz
You can again confirm the exact file name and path via theX-Xdebug-Profiler-Filename header in DevTools, just like in the first test.
2. Comparing Execution Statistics
Open the new profiler snapshot in PHPStorm and switch to Execution Statistics.
Now compare the optimized run with the previous (slow) run:
- The total time of the request is significantly lower.
Vendor\Module\Block\Page::heavyCpuWork()is no longer present in the table.Vendor\Module\Block\Page::getSaleStats()takes much less time and has lower Own Time.- Magento EAV and collection-related methods show fewer calls and reduced time, thanks to:
- a smaller set of selected attributes,
- filtering by
special_priceat the collection level.

The following comparison table summarizes how the optimized version performs against the original slow implementation. It highlights the key differences in execution time, function costs and collection behavior, clearly showing the impact of each change.
| Metric | Before optimization | After optimization |
|---|---|---|
| Template execution time | ~3.982 s | ~0.039 s |
getSaleStats() – Time | high | very low |
getSaleStats() – Own Time | high | very low |
heavyCpuWork() presence | visible, expensive | removed |
| Collection attribute selection | * (all) | specific list |
3. Call Tree: confirming that the hot path is gone
Switching to the Call Tree tab is optional.
In my case, I use the integrated tree view directly inside the Execution Statistics table.
To inspect the slow path, I simply locate the row that corresponds to my custom template and expand it to see the full call structure beneath it.
This reveals exactly how the request flows through the block, including:
- the call to
getSaleStats(), - the internal calls such as
isOnSale(),getProducts(), - and, most importantly, the expensive
heavyCpuWork()method.
Expanding this tree makes the slow functions clearly visible without switching to a different tab.

Final Thoughts and Key Takeaways
Profiling is one of the most reliable ways to understand real performance problems in Magento and other PHP-based systems. In this walkthrough, we intentionally created a slow page, profiled it, analyzed the results, and optimized the code using real metrics instead of assumptions.
Even small custom blocks can hide serious performance costs. A profiler shows the real bottleneck so you optimize based on facts instead of guessing.
Sorting by Own Time instantly reveals which methods consume CPU on their own. This is the quickest way to detect truly expensive code paths.
Most performance issues on custom pages come from unnecessary loops, loading excess data, and heavy calculations inside iterations rather than from Magento itself.
Removing heavy CPU work, limiting attribute selection, and pushing filters into the database can drastically reduce execution time without changing business logic.
Slow page rendering, heavy cron jobs, or unexpected TTFB spikes are all strong signals to start profiling before attempting any optimization.
Profiling turns performance optimization from guesswork into an engineering process driven by real data.
You May Also Find These Articles Useful
A beginner-friendly guide to building your first Magento 2 module from scratch, covering basic structure, registration, and dependency injection.
A real-world performance optimization case with measurable PageSpeed results and practical tuning steps for production Magento stores.