Go beyond add_action and add_filter. Learn how hook priorities affect performance, how to remove expensive callbacks safely, how to build your own hook contracts, and how to profile execution.
Hooks are the backbone of extensibility in WordPress. Mastering them lets you customise behaviour without touching core, and it also gives you levers for performance.
If you have ever looked at a slow page and suspected “some plugin is doing something”, hooks are usually where that something is happening.
Why Hooks Are the Real WordPress API
Themes and plugins rarely slow sites directly. Hooks do.
Every action and filter adds work. Enough of them create invisible complexity that is hard to reason about.
Understanding hooks lets you:
- Remove behaviour cleanly
- Short-circuit expensive logic
- Avoid fragile template overrides
Design Rationale and Trade-offs
Removing hooks is often safer than adding conditionals
Conditionals still run the hooked function. Removal prevents execution entirely.
Priority order matters
Two small callbacks at priority 10 can be worse than one expensive callback at 1.
Global hooks are dangerous
Hooks that fire on every request should do almost nothing.
Practical Guardrails
- Remove behaviour as early as context allows
- Avoid heavy logic in
initandwp_loaded - Document hook removals centrally
Key Takeaways
- Hooks define execution cost
- Fewer callbacks beat clever callbacks
- Predictability beats flexibility
What this post covers
- the practical difference between actions and filters
- priorities, accepted arguments, and how they change execution order
- removing expensive behaviour safely
- designing your own hooks so your theme stays modular
- debugging and profiling hook execution
1. Actions vs filters (in the real world)
Actions run code at a point in time. They do not return a value.
add_action( 'init', 'register_custom_post_type' );
Filters take a value, modify it, and return it.
add_filter( 'the_title', 'prefix_title' );
This matters because filters often run a lot. If your filter is heavy and it runs on every post title in a loop, you have just created a performance problem.
2. Priorities and accepted arguments
Every hook can take:
- a priority (default
10) - the number of accepted arguments (default
1)
add_filter( 'the_content', 'add_cta_block', 20, 1 );
How priority works
- lower number runs earlier
- higher number runs later
- if two callbacks have the same priority, they run in the order they were added
A practical use case: you might want to strip out shortcodes early, then add your own markup later, after other plugins have finished.
Accepted arguments
If a filter passes multiple parameters, you need to declare you accept them:
add_filter( 'excerpt_more', 'custom_ellipsis', 10, 1 );
If you declare 2 accepted args but the filter only passes one, WordPress will not magically invent the second. Always match the hook’s signature.
3. Anonymous functions: great for prototypes, awkward for maintenance
Inline callbacks are concise:
add_action( 'wp_footer', function () {
echo '<!-- tracking -->';
} );
The trade-off: you cannot easily remove them later because you do not have a reference. For production work, prefer named functions or a class method so you can:
- remove it conditionally
- unit test it
- change it without hunting
4. Removing hooks to cut overhead
This is one of the fastest ways to reduce work per request, especially when plugins add behaviour you do not need.
Removing core output
remove_action( 'wp_head', 'wp_generator' );
Removing plugin behaviour conditionally
If a plugin adds an expensive callback that runs on all pages, remove it and only re-add it where you need it.
add_action( 'wp', function () {
if ( ! is_singular( 'product' ) ) {
remove_action( 'wp_enqueue_scripts', 'plugin_enqueue_assets' );
}
}, 20 );
The wp action is a good place for this because WordPress has resolved the main query, so conditionals like is_singular() are reliable.
Performance mindset: remove first, then reintroduce behaviour narrowly.
5. Hooking for performance: do less work earlier
WordPress has early hooks (plugins_loaded, init) and later hooks (wp, template_redirect, wp_enqueue_scripts, wp_footer).
If you do heavy work early, it runs even on requests that will be cached, redirected, or short-circuited. Prefer:
- defer heavy logic until you know you need it
- avoid global work on every request
- bail out fast on irrelevant pages
Example: a complicated navigation build does not need to run on every endpoint, especially not admin-ajax or REST requests.
6. Custom hooks: building a modular theme contract
If you build themes that need to evolve, custom hooks are how you avoid spaghetti templates.
Define hooks around template parts
do_action( 'psy_before_header' );
get_template_part( 'partials/header' );
do_action( 'psy_after_header' );
Then you can add features, swap markup, or integrate plugins without editing template files.
Document your hook contract
If you create custom hooks, document:
- when they fire (which template, which lifecycle stage)
- what arguments are passed
- whether callbacks should echo or return data
That small discipline makes your system maintainable for years.
7. Debugging hook order and execution
Quick introspection
if ( did_action( 'init' ) ) {
error_log( 'init has already run' );
}
error_log( current_filter() );
Tooling that helps
- Query Monitor can show hooks, timing, and related templates.
- Debug Bar is useful on some setups.
- With deeper performance work, APM tooling (New Relic) can reveal time spent inside specific callbacks.
If you suspect a plugin, locate its callbacks by searching for add_action and add_filter in the plugin codebase, then confirm with Query Monitor.
8. Practical patterns I use a lot
A “guard clause” wrapper
function psy_only_on_frontend( callable $callback ): void {
if ( is_admin() ) {
return;
}
$callback();
}
Then use it to wrap frontend-only work.
Namespacing and prefixes
Prefix custom hook names (psy_*) to avoid collisions, especially on multisite and large plugin stacks.
Avoid heavy filters in loops
Filters like the_title, the_content, and the_excerpt can run many times per request. Keep callbacks tiny, and cache computed results if needed.
Real-world application
In a modular theme refactor for a performance project:
- custom hooks were defined around template parts so features could be attached cleanly
- expensive plugin assets were dequeued on pages where they were not used
- priorities were adjusted to avoid duplicated work and conflicting filters
Related reading
FAQ
Is removing hooks “safe”?
Yes, if you understand what you are removing and you test. Remove hooks conditionally and verify page output, SEO metadata, and tracking still work where needed.
Where should I put remove_action calls?
Often inside a callback on wp (or sometimes init) so WordPress conditionals are available. For admin-only removals, use admin_init.
Can hooks really improve performance?
Hooks are not the performance fix by themselves. The performance win comes from using hooks to avoid unnecessary work, stop loading assets, and control when expensive logic runs.
Need WordPress support? I provide maintenance and development for businesses across Cheshire. Learn more about my WordPress services or get in touch.