Daniel really pulled out all the stops when he created the wp profile command – kind of like a New Relic alternative (see this great Kinsta post about using New Relic) it can help pinpoint which components are slowing your WordPress site down. Originally available from runcommand as a premium package, it is now free on github.
If you are looking for a host that allows you to install wp profile check out Kinsta and Cloudways.
I use wp profile regularly to diagnose performance issues for my clients on Codeable. rtcamp used it to identify a bad loop as well, all WordPress developers should be using this! After finishing this guide you will be one step closer to fixing your slow WordPress site.
Using WP-CLI wp profile to Diagnose WordPress Performance Issues
This will show you how to install wp profile and use it effectively to identify performance bottlenecks on your site. I recommend testing wp profile on staging servers so there are no other variables affecting load times.
Install wp-profile package
Fire off this command to install wp profile.
wp package install wp-cli/profile-command
You should see that the process was successful and the wp profile command is now available
Installing package wp-cli/profile-command (dev-master)
Updating /root/.wp-cli/packages/composer.json to require the package...
Using Composer to install the package...
---
Loading composer repositories with package information
Updating dependencies
Resolving dependencies through SAT
Dependency resolution completed in 0.220 seconds
Analyzed 4579 packages to resolve dependencies
Analyzed 190261 rules to resolve dependencies
Package operations: 0 installs, 0 updates, 0 removals
Generating autoload files
---
Success: Package installed.
If you see this error then updating php.ini for cli to have max_memory
or memory_limit = 256M
or higher (up to 2G) should fix it. See this post for more information.
Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 4096 bytes) in phar:///usr/local/bin/wp/vendor/composer/composer/src/Composer/DependencyResolver/RuleWatchGraph.php on line 52
You can update the php-cli’s php.ini to use 1G
for memory_limit
temporarily with this command so you don’t have to edit the php.ini directly
php -d memory_limit=1G $(which wp) package install wp-cli/profile-command --allow-root
If you get the error that /usr/bin/wp
wasn’t found you can find the path to WP-CLI with the which
command
which wp
If wp
isn’t part of your PATH environment variables you will have to specify the path to WP-CLI’s phar here /usr/bin/local/wp
php -d memory_limit=1G /usr/bin/local/wp package install wp-cli/profile-command --allow-root
If you see this error about composer.json
reverted, updating the php.ini max_memory
should help
Updating dependencies
Reverted composer.json.
If you see this error ‘Killed
‘ when trying to install wp profile it can be fixed by using a swap file usually if the memory increase in php.ini doesn’t help.
Installing package wp-cli/profile-command (dev-master)
Updating /root/.wp-cli/packages/composer.json to require the package...
Using Composer to install the package...
---
Loading composer repositories with package information
Updating dependencies
Killed
This creates a 1 GB swap file. You will need root access to create a swap file.
swapoff -a
dd if=/dev/zero of=/swapfile bs=1M count=1024
You will see this output
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 2.16717 s, 495 MB/s
Change the permissions of the swap file and turn it into swap
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
Then re-run the wp profile installation command
wp package install wp-cli/profile-command
Now you should see the successful output
Updating /root/.wp-cli/packages/composer.json to require the package...
Using Composer to install the package...
---
Loading composer repositories with package information
Updating dependencies
Resolving dependencies through SAT
Dependency resolution completed in 0.120 seconds
Analyzed 3696 packages to resolve dependencies
Analyzed 101022 rules to resolve dependencies
Package operations: 1 install, 0 updates, 0 removals
Installs: wp-cli/profile-command:dev-master ef44df5
- Installing wp-cli/profile-command (dev-master ef44df5)
Writing lock file
Generating autoload files
---
Success: Package installed.
Upgrade wp profile
You can upgrade to the latest profile package version with this command
wp package update --allow-root
Using wp profile Command
Enter WP directory (or use --path
at the end of your WP-CLI command)
wp profile can show variable results, it’s best to run it several times when benchmarking!
wp profile stage command
This command shows the stages of loading WordPress.
wp profile stage --allow-root
You will get this very thorough table. Your main focus should generally be the time
column.
In general you want your cache_ratio
to be high like greater than 70%
Generally you want cache_hits
to be greater than cache_misses
Your query_time
should be low as well
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| stage | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | hook_time | hook_count | request_time | request_count |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| bootstrap | 0.8193s | 0.004s | 28 | 92.7% | 559 | 44 | 0.1279s | 2972 | 0s | 0 |
| main_query | 0.0136s | 0.0039s | 7 | 87.25% | 130 | 19 | 0.0042s | 219 | 0s | 0 |
| template | 0.2741s | 0.011s | 30 | 96.94% | 2442 | 77 | 0.2334s | 7030 | 0s | 0 |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| total (3) | 1.107s | 0.0189s | 65 | 92.3% | 3131 | 140 | 0.3654s | 10221 | 0s | 0 |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
Can drill down into each stage, here we drill down into the bootstrap
stage
wp profile stage bootstrap --allow-root
You will then see the hooks used by that stage
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook | callback_count | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before | | 0.3076s | 0.0015s | 4 | 76.47% | 39 | 12 | 0s | 0 |
| muplugins_loaded | 2 | 0.0002s | 0s | 0 | 100% | 2 | 0 | 0s | 0 |
| plugins_loaded:before | | 0.1897s | 0.003s | 12 | 93.28% | 222 | 16 | 0s | 0 |
| plugins_loaded | 33 | 0.0354s | 0s | 0 | 100% | 44 | 0 | 0s | 0 |
| setup_theme:before | | 0.0003s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| setup_theme | 1 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| after_setup_theme:before | | 0.0149s | 0s | 0 | 100% | 40 | 0 | 0s | 0 |
| after_setup_theme | 20 | 0.0068s | 0s | 0 | 100% | 20 | 0 | 0s | 0 |
| init:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| init | 55 | 0.0261s | 0.0009s | 12 | 92.08% | 186 | 16 | 0s | 0 |
| wp_loaded:before | | 0.0001s | 0s | 0 | 100% | 2 | 0 | 0s | 0 |
| wp_loaded | 8 | 0.0001s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp_loaded:after | | 0.0265s | 0s | 0 | | 0 | 0 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (13) | 119 | 0.6078s | 0.0054s | 28 | 95.76% | 559 | 44 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
I usually prefer to use the –-all
flag for the stage command
wp profile stage --all --orderby=time --allow-root
This will show all of the hooks used by each stage
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook | callback_count | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before | | 0.178s | 0.0009s | 1 | 25% | 1 | 3 | 0s | 0 |
| muplugins_loaded | 2 | 0.0003s | 0s | 0 | 50% | 1 | 1 | 0s | 0 |
| plugins_loaded:before | | 0.2855s | 0.0041s | 19 | 83.95% | 136 | 26 | 0s | 0 |
| plugins_loaded | 34 | 0.2442s | 0.0009s | 3 | 98.31% | 116 | 2 | 0s | 0 |
| setup_theme:before | | 0.0005s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| setup_theme | 1 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| after_setup_theme:before | | 0.2653s | 0.0011s | 3 | 99.68% | 615 | 2 | 0s | 0 |
| after_setup_theme | 17 | 0.0241s | 0.0002s | 1 | 98.8% | 82 | 1 | 0s | 0 |
| init:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| init | 82 | 0.261s | 0.0016s | 9 | 98.74% | 703 | 9 | 0s | 0 |
| wp_loaded:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp_loaded | 7 | 0.0012s | 0s | 0 | 100% | 6 | 0 | 0s | 0 |
| parse_request:before | | 0.0192s | 0s | 0 | 100% | 14 | 0 | 0s | 0 |
| parse_request | 2 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| send_headers:before | | 0.0001s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| send_headers | 0 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| pre_get_posts:before | | 0.0003s | 0s | 0 | 100% | 10 | 0 | 0s | 0 |
| pre_get_posts | 8 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| the_posts:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| the_posts | 2 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp | 10 | 0.068s | 0.0148s | 30 | 97.3% | 756 | 21 | 0s | 0 |
| template_redirect:before | | 0.0005s | 0s | 0 | | 0 | 0 | 0s | 0 |
| template_redirect | 20 | 0.0098s | 0.0005s | 1 | 99.06% | 105 | 1 | 0s | 0 |
| template_include:before | | 0.0004s | 0s | 0 | 100% | 9 | 0 | 0s | 0 |
| template_include | 1 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp_head:before | | 0.0028s | 0.0003s | 2 | 85.71% | 18 | 3 | 0s | 0 |
| wp_head | 42 | 0.0887s | 0.004s | 9 | 97.85% | 682 | 15 | 0s | 0 |
| loop_start:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp_footer | 14 | 0.0068s | 0s | 0 | 100% | 61 | 0 | 0s | 0 |
| wp_footer:after | | 0.0002s | 0s | 0 | | 0 | 0 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (31) | 242 | 1.4571s | 0.0284s | 78 | 90.8% | 3323 | 84 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
You can also use the --spotlight
flag to filter out zero-like values for easier reading
wp profile stage --all --spotlight --orderby=time --allow-root
Much cleaner now 🙂
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook | callback_count | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before | | 0.2969s | 0.0016s | 1 | 25% | 1 | 3 | 0s | 0 |
| muplugins_loaded | 2 | 0.0002s | 0s | 0 | 50% | 1 | 1 | 0s | 0 |
| plugins_loaded:before | | 0.2374s | 0.0021s | 19 | 83.95% | 136 | 26 | 0s | 0 |
| plugins_loaded | 34 | 0.2094s | 0.0004s | 3 | 98.31% | 116 | 2 | 0s | 0 |
| after_setup_theme:before | | 0.1991s | 0.0006s | 3 | 99.68% | 615 | 2 | 0s | 0 |
| after_setup_theme | 17 | 0.0299s | 0.0002s | 1 | 98.8% | 82 | 1 | 0s | 0 |
| init | 82 | 0.3435s | 0.001s | 9 | 98.74% | 703 | 9 | 0s | 0 |
| parse_request:before | | 0.0274s | 0s | 0 | 100% | 14 | 0 | 0s | 0 |
| wp | 10 | 0.0747s | 0.0078s | 30 | 97.3% | 756 | 21 | 0s | 0 |
| template_redirect | 20 | 0.0102s | 0.0002s | 1 | 99.06% | 105 | 1 | 0s | 0 |
| wp_head:before | | 0.0039s | 0.0004s | 2 | 85.71% | 18 | 3 | 0s | 0 |
| wp_head | 42 | 0.0884s | 0.0021s | 9 | 97.85% | 682 | 15 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (12) | 207 | 1.5211s | 0.0163s | 78 | 86.2% | 3229 | 84 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
You can use the hook command to drill down into specific hooks.
wp profile hook Command
Here we dig into the wp
hook
wp profile hook wp --allow-root
Here are all the functions being called in the wp hook for this particular site running Avada
+----------------------------------+------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| callback | location | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+----------------------------------+------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| a3_lazy_load_instance() | a3-lazy-load/classes/class-a3-lazy | 0.0002s | 0s | 0 | | 0 | 0 | 0s | 0 |
| | -load.php:540 | | | | | | | | |
| tve_leads_query_group() | thrive-leads/inc/hooks.php:185 | 0.0614s | 0.021s | 30 | 93.1% | 216 | 16 | 0s | 0 |
| tve_leads_one_click_signup() | thrive-leads/inc/hooks.php:2614 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| tve_wp_action() | thrive-visual-editor/plugin-core.p | 0.0006s | 0s | 0 | 100% | 2 | 0 | 0s | 0 |
| | hp:204 | | | | | | | | |
| TCB_Editor->clean_inner_frame() | thrive-visual-editor/inc/classes/c | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| | lass-tcb-editor.php:223 | | | | | | | | |
| Avada_Init->set_theme_version() | Avada/includes/class-avada-init.ph | 0.0017s | 0s | 0 | 64.29% | 9 | 5 | 0s | 0 |
| | p:101 | | | | | | | | |
| Avada_Dynamic_CSS->set_mode() | Avada/includes/class-avada-dynamic | 0.0003s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| | -css.php:62 | | | | | | | | |
| Avada->set_page_id() | Avada/includes/class-avada.php:239 | 0.0004s | 0s | 0 | 100% | 8 | 0 | 0s | 0 |
| Avada_Layout->add_sidebar() | Avada/includes/class-avada-layout. | 0.0251s | 0s | 0 | 100% | 516 | 0 | 0s | 0 |
| | php:20 | | | | | | | | |
| WPSEO_Frontend->page_redirect() | wordpress-seo/frontend/class-front | 0s | 0s | 0 | 100% | 1 | 0 | 0s | 0 |
| | end.php:1377 | | | | | | | | |
+----------------------------------+------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (10) | | 0.0898s | 0.021s | 30 | 93.91% | 756 | 21 | 0s | 0 |
+----------------------------------+------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
Similar to the wp profile stage command, you can use the --all
flag for hooks as well.
wp profile hook --all --spotlight
You can even simulate the loading for a specific URL on the site by using the --url
flag.
wp profile hook --all --spotlight --url=https://guides.wp-bullet.com --orderby=time --allow-root
Looks like only my theme GeneratePress, Yoast and WP Theme Optimizer are being loaded.
+----------------------------------------------+-----------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| callback | location | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+----------------------------------------------+-----------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| wpseo_init() | wordpress-seo/wp-seo-main.php:252 | 0.0136s | 0s | 0 | 100% | 34 | 0 | 0s | 0 |
| wpto_Public->wpto_remove_yoast_information() | wp-theme-optimizer/public/class-wpto-public.php:187 | 0.0151s | 0s | 0 | 100% | 18 | 0 | 0s | 0 |
| wp_enqueue_scripts() | wp-includes/script-loader.php:1294 | 0.015s | 0s | 0 | 100% | 348 | 0 | 0s | 0 |
| generate_construct_header() | generatepress/inc/template-tags.php:514 | 0.0041s | 0.0002s | 2 | 100% | 93 | 0 | 0s | 0 |
| generate_add_navigation_after_header() | generatepress/inc/navigation.php:23 | 0.0132s | 0.0003s | 3 | 100% | 367 | 0 | 0s | 0 |
| wp_trim_excerpt() | wp-includes/formatting.php:3289 | 0.0186s | 0s | 0 | 100% | 16 | 0 | 0s | 0 |
| generate_construct_sidebars() | generatepress/functions.php:434 | 0.0129s | 0.0001s | 1 | 100% | 178 | 0 | 0s | 0 |
+----------------------------------------------+-----------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (7) | | 0.0927s | 0.0006s | 6 | 100% | 1054 | 0 | 0s | 0 |
+----------------------------------------------+-----------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
Let’s compare to my contact page
wp profile hook --all --spotlight --url=https://guides.wp-bullet.com/contact/ --orderby=time --allow-root
Now we can see Pretty Link is running, TablePress and Contact Form 7
+----------------------------------------+-------------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| callback | location | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+----------------------------------------+-------------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| PrliAppController->redirect() | pretty-link/app/controllers/PrliAppController.php:295 | 0.0012s | 0.0003s | 1 | 100% | 2 | 0 | 0s | 0 |
| wpcf7() | contact-form-7/settings.php:88 | 0.012s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wpseo_init() | wordpress-seo/wp-seo-main.php:252 | 0.023s | 0s | 0 | 100% | 34 | 0 | 0s | 0 |
| PrliAppController->install() | pretty-link/app/controllers/PrliAppController.php:253 | 0.014s | 0s | 0 | 100% | 2 | 0 | 0s | 0 |
| TablePress::run() | tablepress/classes/class-tablepress.php:101 | 0.0107s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| wp_enqueue_scripts() | wp-includes/script-loader.php:1294 | 0.0247s | 0.0002s | 1 | 100% | 369 | 0 | 0s | 0 |
| generate_construct_header() | generatepress/inc/template-tags.php:514 | 0.0067s | 0.0003s | 2 | 100% | 97 | 0 | 0s | 0 |
| generate_add_navigation_after_header() | generatepress/inc/navigation.php:23 | 0.0219s | 0.0004s | 3 | 100% | 370 | 0 | 0s | 0 |
| generate_construct_sidebars() | generatepress/functions.php:434 | 0.0167s | 0.0002s | 1 | 100% | 178 | 0 | 0s | 0 |
+----------------------------------------+-------------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (9) | | 0.1309s | 0.0014s | 8 | 100% | 1056 | 0 | 0s | 0 |
+----------------------------------------+-------------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
You can look for the problematic callbacks with grep
, generate_add_navigation_after_header
isn’t problematic, I’m just using it as an example.
grep -ril generate_add_navigation_after_header wp-content/themes
This will spit out the files that contain the callback and you can examine them
wp-content/themes/generatepress/inc/navigation.php
If you get multiple results you can make sort them so only unique (-u
) are shown
grep -ril gettext wp-content/plugins/ | awk -F / '{ print $3}' | sort -u
Another useful grep command that developers can use is rHin which outputs the filename, line numer and before and after characters
grep -rHin generate_add_navigation_after_header wp-content/themes
The file with the occurrence is wp-content/themes/generatepress/inc/navigation.php
The line number is :17:
Where the string is found shows in pink
wp-content/themes/generatepress/inc/navigation.php:17:if ( ! function_exists( 'generate_add_navigation_after_header' ) ) :
wp-content/themes/generatepress/inc/navigation.php:22:add_action( 'generate_after_header', 'generate_add_navigation_after_header', 5 );
wp-content/themes/generatepress/inc/navigation.php:23:function generate_add_navigation_after_header()
Now you can diagnose issues by running the wp profile commands before and after deactivating plugins too! Notice how overall times increase and find the slow code or plugin impairing your WordPress site
wp plugin deactivate plugin-name
Alternatively you can have WP-CLI simulate the deactivation of a plugin like this
wp profile hook --all --spotlight --skip-plugins=plugin-name
Compare the wp profile results before and after, happy WordPress performance troubleshooting :).
If you need help diagnosing the performance issues of your site get in touch with me via Codeable.
Sources
WP-CLI Profile Command – Github
WP-CLI Package Update – WP.org
WP Profile Command – EasyEngine
Identify WordPress Slowness – Daniel Bachhuber
The instructions for making a swap device when wp-cli aborts the package installation, didn’t worked for me, what solved my exhausted memory problem was edit php.ini rasing the memory_limit to 256 M
Also, looks like some lines were collapsed, unless you know what you are doing, pasting the commands won’t work. And it’s not resolved by making swap space, I have more than 4GB and still the sequence didn’t work.
Please force refresh the page, the pre and code tags are working now. You are right, will add the php.ini as well, thank you!
Nice! Now the instructions and profile results are easier to read. Thank you!
Thank you for pointing it out, hope this helps you in some way, have a great weekend 🙂
Instead of changing php.ini you can use php -d argument before like:
php -d memory_limit=256M $PATH_TO_WP/wp
In my case I had allow_url_fopen disabled for security and I had to add allow_url_fopen=1 to be able to install it
How can we profile in the admin to find performance issues?
Hi Mike,
I’m using the profile but it returns no values for cache_ratio, hits nor misses. I don’t know why as I am using LS Cache and it seems to be working properly. Any idea?
I am pretty sure the cache values you see in wp profile are not related to the page cache created by LS Cache but rather WordPress’s internal cache like transients and objects.
Thanks for the fast response. Do you know How can I check if the WordPress internal cache is working properly?