Contents
Introduction
This tutorial shows how to build a Theme Performance Inspector for WordPress using PHP. The inspector gathers key runtime metrics (total page time, PHP execution time, database query counts/times, memory usage, included files, enqueued assets, and outgoing HTTP requests) and renders a lightweight debug panel in the page footer / admin bar for fast inspection during development. The goal is a self-contained, low-dependency tool you can drop into a site or use as a simple plugin while developing themes.
What the inspector measures and why
- Total request time: wall-clock time from plugin initialization to shutdown — helps spot slow pages.
- PHP execution time: high resolution timing for server-side processing.
- Database queries: count and total time (requires SAVEQUERIES to be enabled) — shows query hotspots.
- Memory usage: current and peak memory allocation — useful for spotting memory leaks.
- Included files: files included by PHP during request, filtered to theme/plugin directories as needed.
- Enqueued assets: all scripts and styles listed by WordPress — useful to detect redundant or heavy assets.
- Outgoing HTTP requests: requests made by WP HTTP API — external calls can block page load.
Design considerations
- Only enable the inspector for trusted users (admins) or when WP_DEBUG is true to avoid leaking debug info in production.
- Some features add overhead (notably SAVEQUERIES). Use them only in development environments.
- Keep the output lightweight and non-blocking. Render the inspector at the end of the page (wp_footer) so it doesnt change page flow.
- Collect data in memory, render as an HTML panel. Optionally integrate with admin bar for quick access.
Step-by-step implementation
Below is a complete example plugin you can install under wp-content/plugins/theme-performance-inspector/theme-performance-inspector.php. It is written to be straightforward to inspect and extend. It restricts output to administrators or when a debug flag is enabled.
Important setup notes
- To capture DB query timings, enable query logging by adding the following to wp-config.php (development only):
lt?php // In wp-config.php (development only) define(SAVEQUERIES, true) define(WP_DEBUG, true) ?gt
Full plugin code
Place this file in a plugin folder as described above and activate it. The code uses modern timing APIs (hrtime when available) and falls back to microtime.
lt?php / Plugin Name: Theme Performance Inspector Description: Lightweight runtime inspector for theme performance metrics (queries, memory, included files, enqueued assets, HTTP requests). Version: 1.0 Author: Performance Team / if ( ! defined( ABSPATH ) ) { exit } class Theme_Performance_Inspector { private start_time private events = array() private queries = array() private http_requests = array() private enqueued = array( scripts =gt array(), styles =gt array(), ) private template_file = private enabled = false public function __construct() { // Start time as early as possible this->start_time = this->get_time() // Turn on only for admins or when a debug flag is present if ( ( defined( WP_DEBUG ) WP_DEBUG ) ( is_admin() current_user_can( manage_options ) ) ) { // clamp: allow only to users with manage_options or global WP_DEBUG if ( is_user_logged_in() ) { user = wp_get_current_user() if ( in_array( administrator, (array) user->roles, true ) defined( WP_DEBUG ) WP_DEBUG ) { this->enabled = true } } else { // Also allow for non-logged when WP_DEBUG true if ( defined( WP_DEBUG ) WP_DEBUG ) { this->enabled = true } } } if ( this->enabled ) { add_action( init, array( this, init ) ) add_action( template_include, array( this, capture_template ), 100 ) add_action( wp_print_scripts, array( this, capture_scripts ), 0 ) add_action( wp_print_styles, array( this, capture_styles ), 0 ) add_action( http_api_debug, array( this, http_api_debug ), 10, 5 ) add_action( shutdown, array( this, on_shutdown ) ) add_action( wp_footer, array( this, render_panel ), 9999 ) add_action( admin_bar_menu, array( this, admin_bar_node ), 100 ) } } public function init() { this->mark( init ) } private function get_time() { if ( function_exists( hrtime ) ) { // hrtime(true) returns nanoseconds as integer in PHP7.3 return hrtime( true ) / 1e6 // return ms } return microtime( true ) 1000 // ms } private function mark( label ) { this->events[ label ] = array( time =gt this->get_time(), memory =gt memory_get_usage(), memory_peak =gt memory_get_peak_usage(), ) } public function capture_template( template ) { this->template_file = template this->mark( template_included ) return template } public function capture_scripts() { global wp_scripts if ( ! wp_scripts ) { return } foreach ( wp_scripts-gtqueue as handle ) { if ( isset( wp_scripts-gtregistered[ handle ] ) ) { obj = wp_scripts-gtregistered[ handle ] src = obj-gtsrc if ( empty( src ) ) { src = } else { if ( ! preg_match( #^https?://#, src ) ) { // make absolute path for local scripts src = this->make_absolute_url( src ) } } this->enqueued[scripts][ handle ] = src } } } public function capture_styles() { global wp_styles if ( ! wp_styles ) { return } foreach ( wp_styles-gtqueue as handle ) { if ( isset( wp_styles-gtregistered[ handle ] ) ) { obj = wp_styles-gtregistered[ handle ] src = obj-gtsrc if ( empty( src ) ) { src = } else { if ( ! preg_match( #^https?://#, src ) ) { src = this->make_absolute_url( src ) } } this->enqueued[styles][ handle ] = src } } } private function make_absolute_url( src ) { // Handles relative/absolute paths for the src registered in WP if ( strpos( src, // ) === 0 ) { return is_ssl() ? https: . src : http: . src } // If src starts with /, make absolute with site_url if ( strpos( src, / ) === 0 ) { return site_url( src ) } // Otherwise treat as relative to WP_CONTENT_URL or theme directory if it contains wp-content // Return as-is otherwise. return src } public function http_api_debug( response, type, class, args, url ) { // This action is fired for all WP HTTP requests if WP_HTTP_BLOCK_EXTERNAL is not blocking them. this->http_requests[] = array( url =gt url, class =gt class, type =gt type, args =gt args, response =gt is_array( response ) ? response : null, time =gt this->get_time(), ) } public function on_shutdown() { // Final mark this->mark( shutdown ) // Gather database queries if available global wpdb if ( defined( SAVEQUERIES ) SAVEQUERIES isset( wpdb-gtqueries ) ) { this->queries = wpdb-gtqueries } else { this->queries = array() } // Included files included = get_included_files() // Filter to theme files and plugins or present everything — we show grouped counts this->included_files = included } private function format_ms( ms ) { return round( ms, 3 ) . ms } private function total_time() { start = this->start_time end = isset( this->events[shutdown] ) ? this->events[shutdown][time] : this->get_time() return end - start } private function total_db_time() { time = 0.0 if ( ! empty( this->queries ) is_array( this->queries ) ) { foreach ( this->queries as q ) { // WordPress stores time as last element or as array [query, time] if ( is_array( q ) ) { if ( isset( q[1] ) ) { time = floatval( q[1] ) 1000 // seconds to ms } } } } return time } private function queries_count() { return is_array( this->queries ) ? count( this->queries ) : 0 } public function render_panel() { if ( ! this->enabled ) { return } // Safety: avoid breaking JSON or HTML by escaping values when appropriate total = this->total_time() php_ms = 0 if ( isset( this->events[init] ) ) { php_ms = ( ( isset( this->events[shutdown] ) ? this->events[shutdown][time] : this->get_time() ) - this->events[init][time] ) } db_ms = this->total_db_time() query_count = this->queries_count() memory = memory_get_usage() memory_peak = memory_get_peak_usage() // Build HTML: a small floating panel echo lt!-- Theme Performance Inspector --gt echo ltdiv id=tpi-panel style=position:fixedright:10pxbottom:10pxz-index:99999font-family:sans-seriffont-size:13pxmax-width:520pxbackground:rgba(0,0,0,0.75)color:#fffborder-radius:6pxpadding:10pxbox-shadow:0 2px 10px rgba(0,0,0,0.5)gt echo ltdiv style=display:flexalign-items:centerjustify-content:space-betweenmargin-bottom:6pxgt echo ltdivgtltbgtTheme Performance Inspectorlt/bgtltspan style=color:#cccmargin-left:8pxfont-size:11pxgt (dev)lt/spangtlt/divgt echo ltdiv style=font-size:12pxcolor:#dddgt . esc_html( this->format_ms( total ) ) . lt/divgt echo lt/divgt // Summary echo ltdiv style=display:flexgap:12pxmargin-bottom:8pxgt echo ltdiv style=min-width:120pxgtltbgtPHP:lt/bgt . esc_html( this->format_ms( php_ms ) ) . lt/divgt echo ltdiv style=min-width:120pxgtltbgtDB:lt/bgt . esc_html( this->format_ms( db_ms ) ) . ( . intval( query_count ) . )lt/divgt echo ltdiv style=min-width:120pxgtltbgtMem:lt/bgt . size_format( memory ) . / . size_format( memory_peak ) . lt/divgt echo lt/divgt // Template file echo ltdiv style=margin-bottom:8pxfont-size:12pxcolor:#dddgtltbgtTemplate:lt/bgt . esc_html( this->template_file ) . lt/divgt // Enqueued assets echo ltdetails style=margin-bottom:8pxcolor:#fff opengt echo ltsummary style=cursor:pointeroutline:nonepadding:6px 0color:#ffffont-weight:boldgtEnqueued Assetslt/summarygt echo ltdiv style=max-height:160pxoverflow:autopadding-top:6pxcolor:#dddgt echo ltdiv style=font-weight:boldmargin-bottom:6pxgtScripts ( . count( this->enqueued[scripts] ) . )lt/divgt echo ltul style=padding-left:18pxmargin:0 0 8px 0color:#dddgt foreach ( this->enqueued[scripts] as h =gt src ) { echo ltligtltspan style=color:#9ae6ffgt . esc_html( h ) . lt/spangt nbsp . esc_html( src ) . lt/ligt } echo lt/ulgt echo ltdiv style=font-weight:boldmargin-bottom:6pxgtStyles ( . count( this->enqueued[styles] ) . )lt/divgt echo ltul style=padding-left:18pxmargin:0color:#dddgt foreach ( this->enqueued[styles] as h =gt src ) { echo ltligtltspan style=color:#ffd6a5gt . esc_html( h ) . lt/spangt nbsp . esc_html( src ) . lt/ligt } echo lt/ulgt echo lt/divgt echo lt/detailsgt // DB queries summary (if available) echo ltdetails style=margin-bottom:8pxcolor:#fffgt echo ltsummary style=cursor:pointeroutline:nonepadding:6px 0color:#ffffont-weight:boldgtDatabase querieslt/summarygt echo ltdiv style=max-height:220pxoverflow:autopadding-top:6pxcolor:#dddgt if ( query_count gt 0 ) { echo lttable style=width:100%border-collapse:collapsecolor:#dddfont-size:12pxgt echo lttheadgtlttrgtltth style=text-align:leftpadding:6pxborder-bottom:1px solid rgba(255,255,255,0.08)gt#lt/thgtltth style=text-align:leftpadding:6pxborder-bottom:1px solid rgba(255,255,255,0.08)gtTimelt/thgtltth style=text-align:leftpadding:6pxborder-bottom:1px solid rgba(255,255,255,0.08)gtQuerylt/thgtlt/trgtlt/theadgt echo lttbodygt foreach ( this->queries as i =gt q ) { if ( is_array( q ) ) { sql = isset( q[0] ) ? q[0] : t = isset( q[1] ) ? (float) q[1] 1000 : 0 } else { // some setups store a string - fallback sql = is_string( q ) ? q : t = 0 } idx = intval( i ) 1 echo lttrgt echo lttd style=padding:6pxborder-bottom:1px solid rgba(255,255,255,0.04)font-size:12pxgt . idx . lt/tdgt echo lttd style=padding:6pxborder-bottom:1px solid rgba(255,255,255,0.04)font-size:12pxcolor:#9ae6ffgt . esc_html( this->format_ms( t ) ) . lt/tdgt echo lttd style=padding:6pxborder-bottom:1px solid rgba(255,255,255,0.04)font-size:12pxcolor:#dddgt . esc_html( sql ) . lt/tdgt echo lt/trgt } echo lt/tbodygt echo lt/tablegt } else { echo ltdiv style=color:#bbbfont-size:12pxgtNo queries recorded. Enable SAVEQUERIES in wp-config.php to see per-query timings.lt/divgt } echo lt/divgt echo lt/detailsgt // Outgoing HTTP requests echo ltdetails style=margin-bottom:8pxcolor:#fffgt echo ltsummary style=cursor:pointeroutline:nonepadding:6px 0color:#ffffont-weight:boldgtOutgoing HTTP requests ( . count( this->http_requests ) . )lt/summarygt echo ltdiv style=max-height:160pxoverflow:autopadding-top:6pxcolor:#dddgt if ( ! empty( this->http_requests ) ) { echo ltul style=padding-left:18pxmargin:0color:#dddgt foreach ( this->http_requests as req ) { url = isset( req[url] ) ? req[url] : t = isset( req[time] ) ? req[time] : echo ltli style=margin-bottom:6pxgtltspan style=color:#ffd6a5gt . esc_html( url ) . lt/spangt nbsp . esc_html( t ? date( H:i:s, (int) time() ) : ) . lt/ligt } echo lt/ulgt } else { echo ltdiv style=color:#bbbfont-size:12pxgtNo outgoing requests detected (or http_api_debug not enabled).lt/divgt } echo lt/divgt echo lt/detailsgt // Included files summary included_count = count( this->included_files ) echo ltdetails style=color:#fffgt echo ltsummary style=cursor:pointeroutline:nonepadding:6px 0color:#ffffont-weight:boldgtIncluded Files ( . intval( included_count ) . )lt/summarygt echo ltdiv style=max-height:220pxoverflow:autopadding-top:6pxcolor:#dddgt echo ltul style=padding-left:18pxmargin:0color:#dddfont-size:12pxgt foreach ( this->included_files as f ) { echo ltligt . esc_html( f ) . lt/ligt } echo lt/ulgt echo lt/divgt echo lt/detailsgt // Close panel echo ltdiv style=text-align:rightmargin-top:6pxcolor:#999font-size:11pxgtGenerated by Theme Performance Inspectorlt/divgt echo lt/divgt echo lt!-- End Theme Performance Inspector --gt } public function admin_bar_node( wp_admin_bar ) { if ( ! is_admin_bar_showing() ) { return } total = this->total_time() args = array( id =gt theme_performance_inspector, title =gt Performance: . this->format_ms( total ), meta =gt array( title =gt Theme Performance Inspector ), ) wp_admin_bar-gtadd_node( args ) } } add_action( plugins_loaded, function() { // Instantiate inspector as early as possible global theme_performance_inspector theme_performance_inspector = new Theme_Performance_Inspector() } ) ?gt
How it works — component breakdown
- Initialization and timing: The plugin records start time on instantiation using hrtime() when available (higher precision) events (init, template include, shutdown) are captured by storing timestamps and memory snapshots.
- Template detection: The template_include filter captures the final template file path used by WordPress so you can see which template rendered the page.
- Enqueued assets: The plugin inspects the global wp_scripts and wp_styles objects on wp_print_scripts/styles to list handles and source URLs.
- Database queries: When SAVEQUERIES is enabled, WordPress logs queries to wpdb->queries. The plugin reads this on shutdown and formats each query and its time.
- HTTP requests: The http_api_debug action captures outgoing WP HTTP API traffic so you can inspect external calls during the page request.
- Included files: get_included_files() is used to list all PHP files included during the request. You can filter these to your theme or plugin directory to focus on relevant files.
- Rendering panel: The panel prints at wp_footer with a minimal inline style to avoid additional dependencies. It also adds a summary node to the admin bar for convenience.
Security and performance caveats
- Only activate and show the inspector in development. Exposing internal queries, file paths, and HTTP endpoints on production sites is a security risk.
- Enabling SAVEQUERIES has measurable performance cost. Use it only where necessary.
- The http_api_debug hook will run for each outgoing HTTP request if your theme or plugins make many external calls this can produce huge output. Consider sampling or throttling in large sites.
- Rendering large DB query lists or included-files lists in production pages will bloat HTML. Limit or paginate results for heavy sites.
Extensions and ideas for improvement
- Persist traces for a history view (store JSON snapshots to a secure custom DB table or transient for a short time).
- Offer a filterable panel with search for query text, grouping queries by caller file (requires debug_backtrace integration when queries run).
- Add per-template-part timings by instrumenting calls to get_template_part() if available in your WP version via get_template_part_{slug} actions otherwise wrap key template includes.
- Measure frontend asset sizes by HEAD requests (careful: adds network overhead). For local assets, use file size on disk to estimate payload.
- Integrate with browser-based profiling (send a small JSON blob to a local dev server for visualization) or integrate with existing tools like Query Monitor.
Advanced: measuring template-part render times (approach)
Directly measuring every template-parts rendering time is tricky because WordPress core functions like get_template_part() are not pluggable. Two practical approaches:
- Wrap template includes inside your own theme with helper functions that mark start/end times. For example, in your theme replace calls to get_template_part() with a thin wrapper get_template_part_timed( header ) that records times.
- Use debug_backtrace in combination with SAVEQUERIES and analyze caller stacks when heavy DB queries are executed. For each slow query log the backtrace to identify the template area responsible.
Example wrapper for template parts (theme-side)
lt?php function get_template_part_timed( slug, name = null ) { if ( function_exists( hrtime ) ) { start = hrtime( true ) / 1e6 } else { start = microtime( true ) 1000 } get_template_part( slug, name ) if ( function_exists( hrtime ) ) { end = hrtime( true ) / 1e6 } else { end = microtime( true ) 1000 } // Store somewhere: transient, global, or use a central inspector. if ( isset( GLOBALS[theme_performance_inspector] ) ) { label = name ? {slug}-{name} : slug GLOBALS[theme_performance_inspector]->events[tpl_ . label] = array( start =gt start, end =gt end ) } } ?gt
Where to put this code
- As a plugin: Create the plugin file shown above and enable it in WordPress admin. This is the recommended approach because it keeps the inspector separate from theme code.
- As a mu-plugin: For earliest initialization and if you want it always loaded for development servers, place it in wp-content/mu-plugins.
- Theme integration: You can include smaller portions in your themes functions.php, but avoid shipping the inspector to production themes distributed publicly.
Troubleshooting tips
- If DB query timing shows zero queries even though you expect them, ensure SAVEQUERIES is defined in wp-config.php and that WP_DEBUG is enabled in development.
- No HTTP requests appear? The action http_api_debug fires when WP HTTP API runs and internal code triggers the debug hook ensure outgoing requests are not blocked and the code you expect uses WP HTTP API (wp_remote_get, wp_remote_post, etc.).
- If the inspector panel doesnt show, verify you meet the display conditions: either WP_DEBUG is true or the current user is an admin and logged in.
Summary
This tutorial walked through building a Theme Performance Inspector in PHP for WordPress. The example plugin measures request timing, DB queries, memory usage, enqueued assets, outgoing HTTP requests, and included files. It is deliberately lightweight but extensible — perfect for debugging theme performance during development. Use it as a base to add more advanced profiling, visualization, or automated alerts for regressions.
Useful references
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |