WordPress is a powerhouse, but its “one size fits all” defaults mean it often ships with baggage most modern projects don’t need. Over the years, I’ve refined a specific set of snippets to trim the fat without compromising the core experience.
This guide isn’t just a list of things to delete; it’s a strategic approach to performance. It’s inspired by Terence Eden’s excellent list, which I’ve paired with my own production-tested solutions. Every hook name below was checked against the WordPress 7.1.3 source (the current release on 12 October 2026, per the api.wordpress.org version check), and the snippets that turned out to be stale, wrong or harmful are marked as such. If you would rather hand the job over, see our WordPress development services.
Why debloat WordPress
Why bother with these small scripts? Because they add up. Every unnecessary line is a potential conflict, a tiny rendering delay, or a wasted byte.
What we’re targeting:
- DOM Size, Stripping unused inline styles.
- HTTP Overhead, Dequeuing scripts that shouldn’t be there.
- Security Surface, Disabling legacy protocols like XML-RPC.
- Core Web Vitals, Directly impacting LCP and CLS by removing render-blocking fluff.
[!IMPORTANT] Use Caution: Not every “bloat” is useless. If you rely on the classic editor’s specific styling or built-in emojis for older browsers, some of these fixes might not be for you. Test in staging first.
How to run a complete WordPress debloating script
Add the following to your theme’s functions.php file, or better to a mu-plugin (see the last section) so the code survives a theme switch. Each section is commented with explanations and source links.
1. Remove classic theme styles
WordPress adds “classic-theme-styles” to themes that have no theme.json file. Core only enqueues it when wp_theme_has_theme_json() returns false, so a block theme or a classic theme with theme.json never loads it. If yours does, remove it:
// Remove mandatory classic theme.
function disable_classic_theme_styles() {
wp_deregister_style( "classic-theme-styles" );
wp_dequeue_style( "classic-theme-styles" );
}
add_action( "wp_enqueue_scripts", "disable_classic_theme_styles" );2. Remove emoji support
WordPress prints a small detection script, and only when the browser cannot render current emoji does it load a larger script that swaps emoji characters for SVG images. If your audience uses current browsers, this is unnecessary:
// Remove WP Emoji.
// https://www.denisbouquet.com/remove-wordpress-emoji-code/
remove_action( "wp_head", "print_emoji_detection_script", 7 );
remove_action( "wp_print_styles", "print_emoji_styles" );
remove_action( "admin_print_scripts", "print_emoji_detection_script" );
remove_action( "admin_print_styles", "print_emoji_styles" );
// Blank the SVG base URL passed to the emoji settings.
add_filter( "emoji_svg_url", "__return_false" );
// Stop emoji replacement with images in RSS / Atom Feeds
// https://danq.me/2023/09/04/wordpress-stop-emoji-images/
remove_filter( "the_content_feed", "wp_staticize_emoji" );
remove_filter( "comment_text_rss", "wp_staticize_emoji" );3. Disable automatic text formatting
WordPress “texturizes” your content: straight quotes become curly quotes, and ASCII -- / --- become en dash / em dash characters (U+2013 / U+2014). That breaks code samples and fights an ASCII-hyphen editorial policy (- or --, never long-dash glyphs in source). Disable it on technical content:
// Remove automatic formatting.
// https://css-tricks.com/snippets/wordpress/disable-automatic-formatting/
remove_filter( "the_content", "wptexturize" );
remove_filter( "the_excerpt", "wptexturize" );
remove_filter( "comment_text", "wptexturize" );
remove_filter( "the_title", "wptexturize" );
// More formatting filters.
add_action("init", function() {
remove_filter( "the_content", "convert_smilies", 20 );
foreach ( array( "the_content", "the_title", "wp_title", "document_title" ) as $filter ) {
remove_filter( $filter, "capital_P_dangit", 11 );
}
remove_filter( "comment_text", "capital_P_dangit", 31 );
// Classic-editor sites ONLY: this line stops all block rendering.
remove_filter( "the_content", "do_blocks", 9 );
}, 11);Note: The
capital_P_dangitfilter is WordPress’s way of auto-correctingWordpress(lowercase p) toWordPress. It’s aggressive and sometimes unwanted.
[!WARNING] Removing
do_blocksfromthe_contentmeans block markup is no longer parsed. Dynamic blocks (query loop, latest posts, navigation) render nothing. Keep that line only on a site whose posts were all written in the classic editor.
4. Remove Gutenberg styles and scripts
If you’re using a custom theme and don’t rely on Gutenberg’s frontend styles, remove them. On a block theme, do not: global styles carry the whole theme.json design.
// Remove Gutenberg Styles.
// https://wordpress.org/support/topic/how-to-disable-inline-styling-style-idglobal-styles-inline-css/
remove_action( "wp_enqueue_scripts", "wp_enqueue_global_styles" );
// Core also hooks it to wp_footer at priority 1 for classic themes.
remove_action( "wp_footer", "wp_enqueue_global_styles", 1 );
// Remove Gutenberg editing widgets.
// From https://wordpress.org/plugins/classic-widgets/
// Disables the block editor from managing widgets in the Gutenberg plugin.
add_filter( "gutenberg_use_widgets_block_editor", "__return_false" );
// Disables the block editor from managing widgets.
add_filter( "use_widgets_block_editor", "__return_false" );
// Remove Gutenberg Block Library CSS from loading on the frontend.
// https://smartwp.com/remove-gutenberg-css/
function remove_wp_block_library_css() {
wp_dequeue_style( "wp-block-library" );
wp_dequeue_style( "wp-block-library-theme" );
}
add_action( "wp_enqueue_scripts", "remove_wp_block_library_css", 100 );5. Remove header meta tags
Clean up your <head> section by removing unnecessary meta tags:
// Remove shortlink.
// https://stackoverflow.com/questions/42444063/disable-wordpress-short-links
remove_action( "wp_head", "wp_shortlink_wp_head" );
// Remove RSD.
// https://wpengineer.com/1438/wordpress-header/
remove_action( "wp_head", "rsd_link" );
// Remove extra feed links.
// https://developer.wordpress.org/reference/functions/feed_links/
add_filter( "feed_links_show_comments_feed", "__return_false" );
add_filter( "feed_links_show_posts_feed", "__return_false" );
// Remove api.w.org link.
// https://wordpress.stackexchange.com/questions/211467/remove-json-api-links-in-header-html
remove_action( "wp_head", "rest_output_link_wp_head" );
// https://wordpress.stackexchange.com/questions/211817/how-to-remove-rest-api-link-in-http-headers
// https://developer.wordpress.org/reference/functions/rest_output_link_header/
remove_action( "template_redirect", "rest_output_link_header", 11, 0 );6. Remove image enhancements
WordPress adds srcset, sizes, loading, decoding and fetchpriority attributes to images. If you handle these yourself, remove them, but read the image section further down first, because these attributes protect LCP and CLS:
// Remove WordPress forced image size
// https://core.trac.wordpress.org/ticket/62413#comment:40
add_filter( "wp_img_tag_add_auto_sizes", "__return_false" );
// Remove <img> enhancements
// https://developer.wordpress.org/reference/functions/wp_filter_content_tags/
remove_filter( "the_content", "wp_filter_content_tags", 12 );
// Stop rewriting http:// URLs for the main domain.
// https://developer.wordpress.org/reference/hooks/wp_should_replace_insecure_home_url/
remove_filter( "the_content", "wp_replace_insecure_home_url", 10 );
// Remove the attachment stuff
// https://developer.wordpress.org/news/2024/01/building-dynamic-block-based-attachment-templates-in-themes/
remove_filter( "the_content", "prepend_attachment" );
// Remove the block filter
remove_filter( "the_content", "apply_block_hooks_to_content_from_post_object", 8 );7. Remove browser check and other admin features
// Remove browser check from Admin dashboard.
// https://core.trac.wordpress.org/attachment/ticket/27626/disable-wp-check-browser-version.0.2.php
if ( !empty( $_SERVER["HTTP_USER_AGENT"] ) ) {
add_filter( "pre_site_transient_browser_" . md5( $_SERVER["HTTP_USER_AGENT"] ), "__return_null" );
}
// Remove hovercards on comment links in admin area.
// https://wordpress.org/support/topic/how-to-disable-mshots-service/#post-12946617
add_filter( "akismet_enable_mshots", "__return_false" );What else should you remove from WordPress
Beyond the techniques above, here are my own solutions for further optimization:
8. Disable XML-RPC
If you don’t use external apps to post to WordPress, turn off the XML-RPC methods that require a login, and the pingback methods with them. The xmlrpc_enabled filter on its own does not switch the endpoint off (details below):
// Disable XML-RPC methods that require authentication
add_filter( 'xmlrpc_enabled', '__return_false' );
// Remove the pingback methods, which stay open otherwise
add_filter( 'xmlrpc_methods', function( $methods ) {
unset( $methods['pingback.ping'], $methods['pingback.extensions.getPingbacks'] );
return $methods;
} );
// Remove XML-RPC link from head
remove_action( 'wp_head', 'rsd_link' );9. Remove jQuery Migrate
Modern WordPress themes and plugins rarely need jQuery Migrate. If your site works without it, remove it:
function remove_jquery_migrate( $scripts ) {
if ( ! is_admin() && isset( $scripts->registered['jquery'] ) ) {
$script = $scripts->registered['jquery'];
if ( $script->deps ) {
$script->deps = array_diff( $script->deps, array( 'jquery-migrate' ) );
}
}
}
add_action( 'wp_default_scripts', 'remove_jquery_migrate' );10. Disable oEmbed
If no other site needs to embed your posts, switch off the oEmbed provider side. Embeds of YouTube and other registered providers in your own posts keep working, because WordPress only runs discovery for URLs that are not on its built-in provider list:
// Remove oEmbed discovery links
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
remove_action( 'wp_head', 'wp_oembed_add_host_js' );
// Remove oEmbed REST API endpoint
remove_action( 'rest_api_init', 'wp_oembed_register_route' );
// Disable oEmbed auto discovery
add_filter( 'embed_oembed_discover', '__return_false' );11. Defer JavaScript loading
Add defer to non-critical scripts for better performance. The filter below is the old approach and ignores the dependency rules core applies since WordPress 6.3; the loading strategy section further down shows the supported way:
function add_defer_attribute( $tag, $handle ) {
// Add scripts that should NOT be deferred
$scripts_to_not_defer = array( 'jquery-core' );
if ( in_array( $handle, $scripts_to_not_defer ) || str_contains( $tag, ' defer' ) ) {
return $tag;
}
return str_replace( ' src', ' defer src', $tag );
}
add_filter( 'script_loader_tag', 'add_defer_attribute', 10, 2 );How to measure WordPress debloat impact
After implementing these changes, test your site using:
- Google PageSpeed Insights, Check your Core Web Vitals
- GTmetrix, Detailed waterfall analysis
- WebPageTest, Multiple test locations and connection speeds
You should see:
- Reduced page weight (fewer CSS/JS bytes)
- Fewer HTTP requests
- Better Largest Contentful Paint (LCP) when a removed asset was render-blocking
Server response time (TTFB) barely moves, because none of these snippets changes how PHP builds the page. A caching layer is what moves TTFB.
What a default WordPress 7.1 install loads
Before removing anything, it helps to know what each feature costs. The sizes below were measured on the files inside the official wordpress-7.1.3.zip: raw bytes on disk and the size after gzip at level 9, which is close to what a server sends over the wire.
| Asset | File | Raw | Gzip | When it loads |
|---|---|---|---|---|
| Emoji loader (inline, footer) | wp-emoji-loader.min.js | 2,977 B | 1,290 B | Every front-end page |
| Emoji replacement script | wp-emoji-release.min.js | 22,752 B | 5,310 B | Only when the browser fails the emoji test |
| Classic theme styles | classic-themes.min.css | 291 B | 233 B | Themes without theme.json |
| Combined block library | block-library/style.min.css | 140,829 B | 18,938 B | Only when on-demand block styles are off |
| Block library theme styles | block-library/theme.min.css | 2,752 B | 751 B | Themes that declare wp-block-styles, with on-demand loading off |
| jQuery | jquery.min.js | 87,553 B | 30,356 B | When the theme or a plugin enqueues it |
| jQuery Migrate | jquery-migrate.min.js | 13,577 B | 4,891 B | Together with the jquery handle |
| Dashicons | dashicons.min.css | 59,004 B | 35,693 B | Admin bar for logged-in users, or a plugin |
| Post embed host script | wp-embed.min.js | 1,251 B | 710 B | Only on pages that embed a WordPress post |
| Heartbeat | heartbeat.min.js | 5,947 B | 2,058 B | Admin screens, front end only if a plugin asks |
| Comment reply | comment-reply.min.js | 2,950 B | 1,371 B | When the theme or the Comments block enqueues it |
Two numbers stand out. jQuery plus Migrate is the heaviest thing a classic theme usually ships, so dropping jQuery from your own theme code saves more than every head-tag tweak combined. And the 140 KB block library sheet is no longer loaded wholesale on a default install: block themes have loaded only the styles of blocks present on the page since WordPress 5.9, and since 6.9 core enables the same on-demand loading for classic themes through wp_load_classic_theme_block_styles_on_demand(), as long as the template output buffer is active. A theme opts out by returning false from the should_load_separate_core_block_assets and should_load_block_assets_on_demand filters.
To see what a specific page really loads, use the browser. In Chrome DevTools open the Network tab, reload with the cache disabled, filter by your own domain and sort by size. Lighthouse in the same DevTools panel lists unused CSS and JavaScript per file. WP-CLI does not help here: a wp eval call never renders a template, so the script and style queues it prints are empty. A logged footer hook does work:
// Log every enqueued handle on front-end page views (staging only).
add_action( 'wp_footer', function () {
if ( is_admin() ) {
return;
}
foreach ( wp_scripts()->queue as $handle ) {
error_log( 'script: ' . $handle . ' ' . wp_scripts()->registered[ $handle ]->src );
}
foreach ( wp_styles()->queue as $handle ) {
error_log( 'style: ' . $handle . ' ' . wp_styles()->registered[ $handle ]->src );
}
}, 999 );Which WordPress features to disable, feature by feature
The script above is the quick version. Below, each feature gets the context needed to decide whether it belongs on your site.
What classic theme styles are and what they cost
WordPress 6.1 added a small stylesheet that gives classic themes sensible defaults for block markup, such as button and file block spacing. It weighs 291 bytes and is enqueued by wp_enqueue_classic_theme_styles() only when the active theme has no theme.json. Remove it on a classic theme with its own block CSS. Leave it on classic themes that rely on core block markup looking right without extra work, and do not bother on block themes, where it never loads.
Why WordPress loads an emoji script
The emoji feature does two jobs: it tests whether the browser can draw recent emoji, and if it cannot, it loads wp-emoji-release.min.js to replace the characters with images from s.w.org. In WordPress 7.1.3 the test is an inline type="module" script printed in the footer, so it no longer blocks rendering, and the results are cached in sessionStorage. A small inline stylesheet for img.emoji comes with it. Removing the wp_print_styles action still works: wp_enqueue_emoji_styles() checks for that action and skips the inline CSS when it is gone.
The emoji_svg_url filter now only sets the SVG base URL in the emoji settings. Older guides used it to drop a DNS prefetch to s.w.org; wp_resource_hints() in 7.1.3 no longer adds that hint, so the line is harmless but no longer saves a lookup. Keep emoji support if visitors write in comments or forums from old devices whose fonts lack recent emoji. The complete version also covers e-mail and the classic editor:
function disable_emojis_completely() {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
remove_action( 'embed_head', 'print_emoji_detection_script' );
remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
add_filter( 'tiny_mce_plugins', function ( $plugins ) {
return array_diff( $plugins, array( 'wpemoji' ) );
} );
}
add_action( 'init', 'disable_emojis_completely' );What wptexturize changes in your content
wptexturize() turns straight quotes into typographic ones, -- and --- into en and em dashes, and three dots into an ellipsis character. On technical content it corrupts shortcodes, HTML and PHP fragments that readers copy. The whole function can be switched off with one filter instead of removing it from each hook:
add_filter( 'run_wptexturize', '__return_false' );Keep it only on editorial sites that want typographic quotes and never publish code.
What the capital_P_dangit filter does
capital_P_dangit() replaces Wordpress with WordPress. In titles it replaces every occurrence; in content and comments only after a space, a quote, > or (. It does not touch URLs, because a slash in front of the word does not match. The real risk is code: a sample that contains Wordpress after a space gets changed silently.
Which styles and scripts the block editor adds
On the front end the block editor contributes wp-block-library (the block CSS), wp-block-library-theme (opinionated theme styles) and the inline global-styles generated from theme.json. wp-components belongs to the editor screens, so dequeuing it on the front end does nothing. Older snippets also remove wp_global_styles_render_svg_filters from wp_body_open; that function was deprecated in 6.3.0 and is no longer hooked. Remove block CSS only on sites that do not render blocks on the front end. On a block theme, removing global styles strips the theme’s design.
Which head tags WordPress adds and which to remove
A default wp_head in 7.1.3 prints, among others:
<meta name="generator" content="WordPress 7.1.3" />
<link rel="shortlink" href="https://example.com/?p=123" />
<link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://example.com/xmlrpc.php?rsd" />
<link rel="https://api.w.org/" href="https://example.com/wp-json/" />
<link rel="alternate" type="application/json" href="https://example.com/wp-json/wp/v2/posts/123" />| Tag | Risk | Recommendation |
|---|---|---|
| generator | Low, the version also appears as ?ver= on core assets | Remove if you like, it hides little |
| shortlink | None | Remove |
| RSD (EditURI) | Points clients at xmlrpc.php | Remove with XML-RPC |
| REST API links | Low | Keep if anything discovers your API |
Two removals still seen in many guides do nothing on current versions: wlwmanifest_link was deprecated in 6.3.0 and is no longer printed, and adjacent_posts_rel_link_wp_head has not been used by core since 5.6.0. A current cleanup is shorter:
add_action( 'init', function () {
remove_action( 'wp_head', 'wp_generator' );
remove_action( 'wp_head', 'wp_shortlink_wp_head' );
remove_action( 'template_redirect', 'wp_shortlink_header', 11 );
remove_action( 'wp_head', 'rsd_link' );
} );What WordPress adds to images
wp_filter_content_tags() adds srcset and sizes, width and height where missing, loading="lazy" on images below the fold, decoding="async" and fetchpriority="high" on the likely LCP image. The wp_img_tag_add_auto_sizes filter arrived in 6.7.0, not earlier. Removing these features usually makes CLS and LCP worse, which is the opposite of the goal, so do it only when a CDN or your own markup sets all of them. To switch off lazy loading alone:
add_filter( 'wp_lazy_loading_enabled', '__return_false' );How the dashboard browser check works
wp_check_browser_version() asks api.wordpress.org whether the admin’s browser is outdated and caches the answer as a site transient for one week, keyed by an MD5 of the user agent. It is one remote request per browser per week, not one per admin page view. The PHP version check works the same way, but the transient key is php_check_ plus an MD5 of PHP_VERSION, not of the user agent, so snippets that filter pre_site_transient_phpcompat_ do nothing:
add_filter( 'pre_site_transient_php_check_' . md5( PHP_VERSION ), '__return_null' );Keep that check unless someone else monitors the PHP version, because it is the warning that tells site owners their PHP is out of support.
What the xmlrpc_enabled filter really turns off
XML-RPC is the remote publishing protocol behind older mobile clients, desktop editors and pingbacks. The filter name suggests a master switch, but the WordPress 7.1.3 source says otherwise in the docblock of wp_xmlrpc_server::set_is_enabled():
“Contrary to the way it’s named, this filter does not control whether XML-RPC is fully enabled, rather, it only controls whether XML-RPC methods requiring authentication - such as for publishing purposes - are enabled.”
So after add_filter( 'xmlrpc_enabled', '__return_false' ), xmlrpc.php still answers and pingback.ping still works. Pingbacks are the part abused for reflected traffic, and system.multicall lets one request carry many login attempts, which the filter does stop because those methods need authentication. The snippet in section 8 also unsets the pingback methods through xmlrpc_methods. The X-Pingback HTTP header is only sent on single posts that allow pings, so closing pings (next sections) removes it too. To take the endpoint off completely, deny xmlrpc.php at the web server; then check any plugin or app that publishes remotely, because it will stop working.
What jQuery Migrate is for
jQuery Migrate restores jQuery APIs that were removed in later jQuery versions and logs warnings when old code calls them. WordPress registers the jquery handle as an alias of jquery-core plus jquery-migrate, which is why the snippet in section 9 edits the dependency list instead of dequeuing a file. Before removing it on production, load the site on staging with define( 'SCRIPT_DEBUG', true );, which makes WordPress serve the unminified jquery-migrate.js that core keeps for debugging, and read the console for JQMIGRATE warnings about deprecated calls. Do not call wp_deregister_script( 'jquery-migrate' ) on the front end; any plugin that declares jquery-migrate as a dependency would then lose its own script, because WordPress does not print a script whose dependency is missing.
How oEmbed works in WordPress
oEmbed has two sides. As a consumer, WordPress turns a pasted YouTube or Vimeo link into a player using its built-in provider list; that keeps working after the snippet in section 10. As a provider, your site prints discovery links and serves /wp-json/oembed/1.0/embed so other WordPress sites can embed your posts. The wp-embed script (710 bytes gzipped) is enqueued only on pages that contain such a post embed. Two lines that appear in older “disable embeds” code should not be copied: removing wp_filter_oembed_result from oembed_dataparse turns off the sanitising that core applies to HTML from providers that are not on the trusted list, and the wpembed TinyMCE plugin no longer exists, so filtering it out of tiny_mce_plugins does nothing.
How to restrict the REST API without breaking the block editor
The REST API answers at /wp-json/ and is what the block editor, the site editor and many plugins use. The official REST API handbook is explicit in its frequently asked questions:
“You should not disable the REST API; doing so will break WordPress Admin functionality that depends on the API being active.”
What the handbook offers instead is requiring authentication through the rest_authentication_errors filter. Logged-in editors keep working, anonymous requests get a 401:
add_filter( 'rest_authentication_errors', function( $result ) {
// If a previous authentication check was applied,
// pass that result along without modification.
if ( true === $result || is_wp_error( $result ) ) {
return $result;
}
// No authentication has been performed yet.
// Return an error if user is not logged in.
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
__( 'You are not currently logged in.' ),
array( 'status' => 401 )
);
}
// Our custom authentication check should have no effect
// on logged-in requests
return $result;
});This breaks every front-end feature that calls /wp-json/ for anonymous visitors: forms that submit through REST, search or filter widgets that fetch JSON, a headless front end, and other sites embedding your posts through the oEmbed endpoint. A narrower option is hiding single routes with the rest_endpoints filter, for example the users endpoint that lists author slugs. Test every form and interactive block while logged out after the change.
How to tune the WordPress Heartbeat API
Heartbeat is a polling loop in heartbeat.js that sends a request to admin-ajax.php at a fixed interval. It carries post locking (the “someone else is editing” notice), autosave data in the classic editor, and the login expiry check that shows the re-login dialog. In WordPress 7.1.3 the default mainInterval is 60 seconds, the allowed range is 1 to 3,600 seconds, the loop can speed up to 5 seconds temporarily, and it slows to 120 seconds when the tab is hidden. Setting the interval to 60 therefore changes nothing; a slower interval is the useful knob on busy admin teams or weak hosting:
add_filter( 'heartbeat_settings', function ( $settings ) {
$settings['interval'] = 120;
return $settings;
} );On the front end, core does not load Heartbeat for visitors; it appears only when a plugin enqueues it. Deregistering it there is safe if you check that plugin’s feature afterwards. Never deregister it in the admin: post locks and session expiry warnings depend on it.
How to limit post revisions with WP_POST_REVISIONS
Revisions do not slow down the front end, because they are never queried on public pages. They grow the wp_posts table and slow down backups and exports. The control is a constant in wp-config.php, read by wp_revisions_to_keep():
define( 'WP_POST_REVISIONS', 10 ); // keep the 10 newest revisions per post
// define( 'WP_POST_REVISIONS', false ); // no revisions at allThe default is true, which means unlimited. false or 0 disables revisions; a positive number keeps that many. The wp_revisions_to_keep filter overrides the constant, and since WordPress 5.8 the wp_{$post_type}_revisions_to_keep filter sets a limit per post type. The limit applies when a post is next saved, so existing extra revisions stay until then. Turning revisions off removes the safety net for editors, so a small number is usually the better trade.
Autosave has its own constant. There is no autosave_interval filter in core, so code that adds one does nothing; set the interval in seconds in wp-config.php instead:
define( 'AUTOSAVE_INTERVAL', 120 ); // default is 60How to disable comments across the whole site
A site without comments still renders comment forms where the theme asks for them, keeps the comment feed link in the head and keeps the pingback machinery. Closing comments and pings through filters covers every post type without editing posts in the database:
add_filter( 'comments_open', '__return_false', 20, 2 );
add_filter( 'pings_open', '__return_false', 20, 2 );
add_filter( 'feed_links_show_comments_feed', '__return_false' );
add_action( 'admin_menu', function () {
remove_menu_page( 'edit-comments.php' );
} );
add_action( 'init', function () {
remove_post_type_support( 'post', 'comments' );
remove_post_type_support( 'page', 'comments' );
} );Closing pings also removes the X-Pingback header, since core only sends it when pings_open() is true. Existing comments stay in the database and may still show if the theme prints them regardless of comments_open(). The comment-reply script is not worth separate work: core registers it with the async strategy and low fetch priority, and it is 1,371 bytes gzipped.
How to defer scripts with the loading strategy API
Since WordPress 6.3, wp_register_script() and wp_enqueue_script() accept a strategy argument with the values defer or async. Core then checks the dependency tree before printing the attribute: a script with an inline script attached in the after position is never delayed, and a defer script falls back to blocking when a dependent script needs it to run first. That logic lives in WP_Scripts::get_eligible_loading_strategy(). The script_loader_tag filter in section 11 skips all of it, which is how deferred scripts end up running after inline code that calls them.
// Your own script.
wp_enqueue_script( 'site-main', get_theme_file_uri( 'js/main.js' ), array(), '1.4.0', array(
'in_footer' => true,
'strategy' => 'defer',
) );
// A script registered by a plugin.
add_action( 'wp_enqueue_scripts', function () {
wp_script_add_data( 'some-plugin-handle', 'strategy', 'defer' );
}, 20 );View the page source afterwards: if core decided a handle cannot be deferred, the attribute is simply missing.
How to test WordPress performance before and after debloating
One run of PageSpeed Insights is not a measurement, because lab scores move between runs. A procedure that gives comparable numbers:
- Prepare: clear the page cache, use the same hosting and the same URL set for both runs, and note the WordPress, theme and plugin versions.
- Baseline: run PageSpeed Insights or Lighthouse three times per URL and keep the median, then record transferred bytes and request count from the DevTools Network tab with the cache disabled.
- Deploy the debloat code to staging and clear the cache again.
- Repeat the same runs on the same URLs.
- Compare transferred bytes, request count, LCP and CLS. Field data in the Chrome UX Report section of PageSpeed Insights updates over 28 days, so check it again a month later.
The size table above tells you the upper bound. On a classic theme that already loads block styles on demand, removing emoji, classic theme styles and the head tags saves a few kilobytes and some lines of HTML; jQuery and plugin assets are where the large numbers are.
How to check compatibility after debloating
After each change, check three areas:
- Front end: the layout, menus, sliders and forms, and the browser console for JavaScript errors.
- Admin: the post editor, the media library, the widget screen and every plugin settings page you use.
- Logged out: contact forms, comments if you keep them, search and anything that loads data without a page reload.
Turn on logging while you test, and keep display off so visitors do not see notices:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );| Problem | Likely cause | Fix |
|---|---|---|
| jQuery errors in the console | jQuery Migrate removed | Restore Migrate or update the plugin that calls deprecated APIs |
| Blocks look unstyled | Block library or global styles removed | Restore the styles on block content pages |
| Query loop or navigation renders nothing | do_blocks removed from the_content | Remove that line |
| Form submits fail when logged out | REST API limited to logged-in users | Allow the plugin’s route or drop the restriction |
| Mobile app or remote publisher fails | XML-RPC blocked | Re-enable it or switch the client to application passwords and REST |
| Embeds of your posts on other sites break | oEmbed provider side removed | Expected; restore if partners need it |
How to package the debloat code as a mu-plugin
Code in functions.php disappears when the theme changes. A must-use plugin in wp-content/mu-plugins/ loads on every request, cannot be deactivated from the admin, and keeps the debloat settings independent of the theme. WordPress loads PHP files placed directly in that folder, not files in subfolders, so either keep one file or add a small loader:
wp-content/
mu-plugins/
debloat-loader.php
debloat/
emoji.php
head-cleanup.php
xmlrpc.php
heartbeat.php<?php
/**
* Plugin Name: Debloat
* Description: Removes front-end features this site does not use.
*/
defined( 'ABSPATH' ) || exit;
foreach ( array( 'emoji', 'head-cleanup', 'xmlrpc', 'heartbeat' ) as $module ) {
$file = __DIR__ . '/debloat/' . $module . '.php';
if ( is_readable( $file ) ) {
require_once $file;
}
}Each module holds the snippets from the sections above. Keep two things out of a debloat plugin. Turning off automatic plugin and theme updates with auto_update_plugin and auto_update_theme is a maintenance policy, not a performance setting, and it switches off security releases along with feature releases. And revision and autosave limits are constants, so they belong in wp-config.php, not in a plugin that loads after those constants are read.
Summary
WordPress’s “batteries included” approach is great for beginners, but developers seeking maximum performance need to trim the fat. The techniques in this guide, inspired by Terence Eden’s excellent debloating script, will help you achieve a leaner, faster WordPress installation.
Several classic snippets now do nothing on WordPress 7.1 (WLW manifest, adjacent post links, SVG filters, the autosave_interval filter), and some do harm (removing do_blocks, removing oEmbed sanitising, blocking the REST API outright). Every line of code you remove is one less potential point of failure, so check each snippet against the version you run before it goes into production.







