Case
WordPress Multisite optimization
Dashboard from 12 seconds to 1
12 s
before the optimization
1 s
after the optimization
4
measures taken
Context
A WordPress Multisite network with thousands of sites running on the same core. I wrote a PHP module that adjusts how WordPress behaves through filters and actions, without changing any core files.
The problem
The network admin dashboard took up to 12 seconds to load. In a network with thousands of sites, several WordPress and plugin routines ask 'which sites does this user have access to?' and build something for each one. That cost repeats on every dashboard screen.
Dashboard load time
Real-time simulation. The 'before' takes about 12 seconds to finish.
- Before 12 s
- After 1 s
What cut the dashboard time
I implemented 4 measures, each independent of the others. Below is what each one does and why it helps in a dashboard that serves thousands of sites.
- WordPress core
One site instead of thousands
WordPress queries the user's site list (the get_blogs_of_user function) on several screens and also uses it to build the 'My Sites' menu in the admin bar. With a filter (pre_get_blogs_of_user), I return only the site the person is on, instead of thousands. I also removed the menu, which would stay heavy wherever the filter is turned off.
PHP final class LimitBlogsToCurrentSite implements Optimization { // Only runs on a Multisite network and outside the screens that need the full site list. public function isApplicable(): bool { return is_multisite() && !(defined("WP_CLI") && WP_CLI) && !wp_doing_cron() && !is_network_admin() && !is_user_admin() && ($GLOBALS["pagenow"] ?? "") !== "my-sites.php"; } // WordPress filter that decides a user's site list, before it is queried from the database. public function register(): void { add_filter("pre_get_blogs_of_user", array($this, "filter"), 10, 2); } // Replaces the whole list with a single item: the site the person is on. public function filter($preloaded, $userId) { // Only touches the list of the logged-in user; other users' lists stay as they are. if ((int) $userId !== get_current_user_id()) { return $preloaded; } // Current site. If there is none, keeps the default WordPress behavior. $site = get_site(); if (!$site) { return $preloaded; } // Returns only the current site, in the format WordPress expects. return array((int) $site->blog_id => $this->toUserBlog($site)); } // Builds the item with the same fields WordPress would use, so the rest of the system notices no difference. private function toUserBlog($site): \stdClass { return (object) array( "userblog_id" => (int) $site->blog_id, "blogname" => $site->blogname, "domain" => $site->domain, "path" => $site->path, "site_id" => (int) $site->site_id, "siteurl" => $site->siteurl, "archived" => $site->archived, "spam" => $site->spam, "deleted" => $site->deleted, ); } } // Complement: removes the 'My Sites' menu across the whole network, including where the filter above is turned off. final class RemoveMySitesMenu implements Optimization { public function isApplicable(): bool { return is_multisite(); } // Removes the 'My Sites' menu from the admin bar. public function register(): void { // Runs at priority 1, before WordPress builds the menu (it builds at priority 20). add_action("admin_bar_menu", function () { remove_action("admin_bar_menu", "wp_admin_bar_my_sites_menu", 20); }, 1); } }How this helps The cost of building the user's site list grew with the network. Now the dashboard works with just one, and the 'My Sites' menu (one item per site) is no longer built.
Why it does no harm The filter only changes the user's own list, and permissions stay the same. The full list remains in the network dashboard and on the sites page, where you switch sites. Only the shortcut is lost.
- Simple History plugin
Simple History network menu
Simple History, a plugin that records the history of actions, adds a network item to the admin bar. I removed that item with a callback that finds the plugin instance and turns it off.
PHP // Removes the network item that the Simple History plugin adds to the admin bar. final class RemoveSimpleHistoryNetworkMenu implements Optimization { // Plugin class and method that create this item. private const CLASS_NAME = "Simple_History\\Services\\Network_Menu_Items"; private const METHOD = "add_admin_bar_network_menu_item"; public function isApplicable(): bool { return true; } public function register(): void { // The callback lives inside a plugin object, so I use a generic remover (below). $remover = new InstanceCallbackRemover("admin_bar_menu", self::CLASS_NAME, self::METHOD); // Very low priority: runs before the plugin and removes its callback before it is executed. add_action("admin_bar_menu", array($remover, "remove"), -9999999); } } // Generic remover: deletes a callback that belongs to an object of a class. final class InstanceCallbackRemover { private $hook; private $className; private $method; public function __construct(string $hook, string $className, string $method) { $this->hook = $hook; $this->className = $className; $this->method = $method; } // The regular remove_action needs the object's own instance, which we do not have here. So I walk the hook's callbacks and delete the one that matches the class and method. public function remove(): void { global $wp_filter; if (empty($wp_filter[$this->hook])) { return; } foreach ($wp_filter[$this->hook]->callbacks as $priority => $callbacks) { foreach ($callbacks as $id => $callback) { if ($this->matches($callback["function"])) { unset($wp_filter[$this->hook]->callbacks[$priority][$id]); } } } } // Checks whether the callback is a method of the given class and name. private function matches($function): bool { return is_array($function) && is_object($function[0]) && get_class($function[0]) === $this->className && $function[1] === $this->method; } }How this helps The plugin builds that item every time the admin bar is drawn. Without the item, that routine no longer runs on every dashboard screen.
Why it does no harm Simple History stays active and keeps recording the history as usual, and the plugin's own screens remain accessible. Only the network menu item in the admin bar goes away.
- WordPress core
Admin command palette turned off
The command palette (the dashboard's search shortcut) loads scripts and assets on every admin screen. I turned it off.
PHP final class RemoveCommandPalette implements Optimization { // Admin dashboard only. public function isApplicable(): bool { return is_admin(); } public function register(): void { // Runs early (priority -1), before WordPress loads the command palette scripts. add_action("admin_enqueue_scripts", function () { // Without that load, the palette no longer exists and each dashboard screen downloads fewer scripts. remove_action("admin_enqueue_scripts", "wp_enqueue_command_palette_assets"); }, -1); } }How this helps The palette loads with its own scripts and assets on every dashboard screen. Without it, each screen downloads and processes fewer files.
Why it does no harm It is only a keyboard search shortcut. All dashboard menus and screens stay exactly as they were. All that is lost is that shortcut.
- Yoast SEO plugin
Yoast indexing notice removed
Yoast SEO creates an 'optimize SEO data' notice (the indexing notification) on dashboard screens. I removed that notice, which was running on the dashboard of every site in the network.
PHP final class RemoveYoastIndexingNotification implements Optimization { // Yoast SEO class and method that create the indexing notice. private const CLASS_NAME = "Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Notification_Integration"; private const METHOD = "maybe_create_notification"; // Admin dashboard only. public function isApplicable(): bool { return is_admin(); } public function register(): void { // Same generic remover used in Simple History. $remover = new InstanceCallbackRemover("admin_init", self::CLASS_NAME, self::METHOD); // Runs before Yoast, so the notice is never created. The plugin keeps working normally. add_action("admin_init", array($remover, "remove"), -9999999); } }How this helps Yoast runs a routine to decide whether to show an SEO data optimization notice on dashboard screens. By stopping the notice from being created, that routine no longer runs.
Why it does no harm Yoast SEO keeps working and generating the SEO information for the pages. Only the notice disappears, which is a notification for the administrator, not a plugin feature.
Result
The admin dashboard dropped from up to 12 seconds to about 1 second.
Technologies
- PHP
- WordPress
- Hooks e filtros
- Multisite
Contact
Shall we talk?
A role, an opportunity or just a technical question. Tell me what you have in mind and I'll reply by email.