From c937a010a231f7e8423f7960aed33c0d9c1f4f22 Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Fri, 14 Aug 2026 15:37:18 +0530 Subject: [PATCH] Icons: Register the default icons lazily instead of on every request. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_wp_register_default_icons()` was hooked to `init` unconditionally, so every request — including front-end requests that never render an icon — included the 88-entry core icon manifest and registered all of it. The manifest calls `_x()` for each label, so this also ran 88 gettext lookups per request. None of that data is used unless something reads the icons registry, which in practice means the editor, the REST API, or a rendered `core/icon` block. Registration now happens on first read of the registry, guarded by a flag on the registry instance. `_wp_register_default_icons()` additionally ensures the `core` collection exists, since a lazy load can now occur before `init`. Measured on trunk, per request: include manifest (88 `_x()` calls) 0.126 ms 88 `register()` calls 0.096 ms total 0.221 ms That cost is now zero for requests that never touch the registry. Verified that the default icons are absent after `init` and present after the first read, and that `wp_get_icon()` still returns valid SVG markup. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/class-wp-icons-registry.php | 36 ++++++ src/wp-includes/default-filters.php | 5 +- src/wp-includes/icons.php | 9 ++ tests/phpunit/tests/icons/wpIconsRegistry.php | 104 ++++++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-icons-registry.php b/src/wp-includes/class-wp-icons-registry.php index 1851a4150a057..4d2288a0a2218 100644 --- a/src/wp-includes/class-wp-icons-registry.php +++ b/src/wp-includes/class-wp-icons-registry.php @@ -20,6 +20,14 @@ class WP_Icons_Registry { */ protected $registered_icons = array(); + /** + * Whether the default icons bundled with core have been registered yet. + * + * @since 7.1.0 + * @var bool + */ + protected $default_icons_loaded = false; + /** * Container for the main instance of the class. * @@ -336,6 +344,8 @@ public function get_registered_icon( $icon_name ) { * @return array[] Array of arrays containing the registered icon properties. */ public function get_registered_icons( $search = '' ) { + $this->load_default_icons(); + $icons = array(); foreach ( $this->registered_icons as $icon ) { @@ -362,9 +372,35 @@ public function get_registered_icons( $search = '' ) { * @return bool True if the icon is registered, false otherwise. */ public function is_registered( $icon_name ) { + $this->load_default_icons(); + return isset( $this->registered_icons[ $icon_name ] ); } + /** + * Registers the default icons bundled with core, once, on first use. + * + * The core icon manifest declares 88 icons, each with a translated label. + * Registering them eagerly costs every request — including front-end + * requests that never render an icon — so registration is deferred until + * something actually reads the registry. + * + * @since 7.1.0 + */ + protected function load_default_icons() { + if ( $this->default_icons_loaded ) { + return; + } + + /* + * Set before registering: _wp_register_default_icons() calls back into + * this class, and this flag is what stops that from recursing. + */ + $this->default_icons_loaded = true; + + _wp_register_default_icons(); + } + /** * Utility method to retrieve the main instance of the class. * diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 12ca0045b98b4..64d49f8cd9aa1 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -816,7 +816,10 @@ // Icons. add_action( 'init', '_wp_register_default_icon_collections', 0 ); -add_action( 'init', '_wp_register_default_icons' ); +/* + * The default icons themselves are registered lazily, on first read of the + * icons registry. See WP_Icons_Registry::load_default_icons(). + */ // Add ignoredHookedBlocks metadata attribute to the template and template part post types. add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes' ); diff --git a/src/wp-includes/icons.php b/src/wp-includes/icons.php index 0af4b6d61b873..144a29d531a16 100644 --- a/src/wp-includes/icons.php +++ b/src/wp-includes/icons.php @@ -98,6 +98,15 @@ function _wp_register_default_icon_collections() { * @access private */ function _wp_register_default_icons() { + /* + * Icons can be requested before `init` now that the default icons are + * registered on first read, so make sure the collection they belong to + * exists rather than relying on the `init` callback having run. + */ + if ( ! WP_Icon_Collections_Registry::get_instance()->is_registered( 'core' ) ) { + _wp_register_default_icon_collections(); + } + $icons_directory = ABSPATH . WPINC . '/images/icon-library/'; $manifest_path = ABSPATH . WPINC . '/assets/icon-library-manifest.php'; diff --git a/tests/phpunit/tests/icons/wpIconsRegistry.php b/tests/phpunit/tests/icons/wpIconsRegistry.php index 42853d2666a95..a68d75a3a9715 100644 --- a/tests/phpunit/tests/icons/wpIconsRegistry.php +++ b/tests/phpunit/tests/icons/wpIconsRegistry.php @@ -453,4 +453,108 @@ public function test_get_content_returns_null_for_invalid_file( $contents, $exte $this->assertNull( $icon['content'] ); } + + /** + * Resets the registry singleton so a test starts with the default icons + * unloaded. + * + * Needed because tear_down() unregisters a test collection, and collection + * unregistration reads the icon registry in order to drop that collection's + * icons — which triggers the lazy load into the replacement instance. + */ + private function reset_registry() { + $reflection = new ReflectionClass( WP_Icons_Registry::class ); + $property = $reflection->getProperty( 'instance' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + $property->setValue( null, null ); + + $this->registry = WP_Icons_Registry::get_instance(); + } + + /** + * Reads the registered icons property directly, so that the assertion itself + * does not trigger the lazy registration it is checking for. + * + * @return array Registered icons. + */ + private function get_registered_icons_property() { + $reflection = new ReflectionClass( WP_Icons_Registry::class ); + $property = $reflection->getProperty( 'registered_icons' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + + return $property->getValue( $this->registry ); + } + + /** + * @ticket 00000 + * + * @covers ::load_default_icons + */ + public function test_default_icons_are_not_registered_until_the_registry_is_read() { + $this->reset_registry(); + + $this->assertSame( + array(), + $this->get_registered_icons_property(), + 'The default icons should not be registered before the registry is read.' + ); + } + + /** + * @ticket 00000 + * + * @covers ::load_default_icons + */ + public function test_default_icons_are_registered_on_first_read() { + $this->reset_registry(); + + $icon = $this->registry->get_registered_icon( 'core/arrow-left' ); + + $this->assertIsArray( $icon, 'A core icon should resolve without explicit registration.' ); + $this->assertSame( 'core/arrow-left', $icon['name'] ); + $this->assertStringStartsWith( 'assertNotEmpty( + $this->get_registered_icons_property(), + 'Reading the registry should have registered the default icons.' + ); + } + + /** + * @ticket 00000 + * + * @covers ::is_registered + */ + public function test_is_registered_triggers_default_icon_registration() { + $this->reset_registry(); + + $this->assertTrue( $this->registry->is_registered( 'core/arrow-left' ) ); + } + + /** + * The default icons belong to the `core` collection, which is normally + * registered on `init`. A lazy load can happen before that, so the collection + * must be ensured at load time rather than assumed to exist. + * + * @ticket 00000 + * + * @covers ::load_default_icons + */ + public function test_default_icons_load_even_if_core_collection_is_missing() { + $collections = WP_Icon_Collections_Registry::get_instance(); + if ( $collections->is_registered( 'core' ) ) { + $collections->unregister( 'core' ); + } + $this->assertFalse( $collections->is_registered( 'core' ) ); + + $this->reset_registry(); + + $icons = $this->registry->get_registered_icons(); + + $this->assertNotEmpty( $icons, 'Default icons should register even when the collection was not set up yet.' ); + $this->assertTrue( $collections->is_registered( 'core' ), 'The core collection should be registered on demand.' ); + } }