Skip to content

Icons: Register the default icons lazily instead of on every request - #78

Draft
mukeshpanchal27 wants to merge 1 commit into
trunkfrom
perf/lazy-icon-registration
Draft

Icons: Register the default icons lazily instead of on every request#78
mukeshpanchal27 wants to merge 1 commit into
trunkfrom
perf/lazy-icon-registration

Conversation

@mukeshpanchal27

Copy link
Copy Markdown
Owner

What

Defers registration of the 88 core icons until something actually reads the icons registry, instead of doing it on init for every request.

Why

default-filters.php hooked registration unconditionally:

add_action( 'init', '_wp_register_default_icon_collections', 0 );
add_action( 'init', '_wp_register_default_icons' );

_wp_register_default_icons() includes a 10.5 KB manifest and registers all 88 icons through WP_Icons_Registry::register(), which per icon runs two preg_match validations, an array_fill_keys allocation, and a key-by-key property check. The generated manifest also calls _x() for every label, so 88 gettext lookups run on every request.

None of it is used unless something reads the registry — in practice the editor, the REST API, or a rendered core/icon block. A front-end page view that renders no icon pays the full cost and discards it.

Measured on trunk, in a booted WordPress (PHP 8.3.2, 2000 iterations, warmed):

Component Cost per request
include manifest (88 _x() calls) 0.126 ms
88 register() calls 0.096 ms
Total 0.221 ms

How

A $default_icons_loaded flag on the registry, and a load_default_icons() guard called from the read paths — is_registered() and get_registered_icons(). get_registered_icon() and unregister() both route through is_registered(), so they are covered too.

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();
}

_wp_register_default_icons() now also ensures the core collection exists, because a lazy load can occur before init fires:

if ( ! WP_Icon_Collections_Registry::get_instance()->is_registered( 'core' ) ) {
	_wp_register_default_icon_collections();
}

Collection registration itself stays on init — it registers one collection with two __() calls, which is not worth deferring.

Behaviour notes

Worth reviewer attention, since these are real changes rather than pure optimisation:

  • register() still triggers the load, via its existing is_registered() duplicate check. This is deliberate: it keeps duplicate detection semantically identical to today. The trade-off is that a plugin registering its own icon on init will pull in the core defaults and forfeit the saving for that site. Making register() use a direct isset() would avoid that, but would change which party gets the "already registered" notice, so I left it alone.
  • Unregistering an icon collection now triggers the load. WP_Icon_Collections_Registry::unregister() enumerates registered icons to drop the ones in that collection, and that read populates the defaults first. Unavoidable — you need the full list to know what to remove.
  • _wp_register_default_icons() no longer runs on init. Anything depending on that specific timing would be affected. It remains callable directly and is @access private.

Testing

Four tests added to tests/phpunit/tests/icons/wpIconsRegistry.php, covering: defaults absent before a read, present after a read, is_registered() triggering the load, and the load working when the core collection has not been registered yet.

They reset the registry singleton explicitly rather than relying on set_up(), because the existing tear_down() unregisters a test collection after nulling the singleton — and that unregistration reads the registry, lazily populating the replacement instance. Without the explicit reset the assertions leak across tests.

Existing suites — all green (MySQL 8.4, PHP 8.3.2), 1868 tests total:

Suite Result
tests/phpunit/tests/icons/ OK — 107 existing + 4 new
tests/phpunit/tests/blocks/ OK (999)
tests/phpunit/tests/rest-api/ OK
tests/phpunit/tests/block-supports/ OK

Behaviour verified directly. Reading registered_icons reflectively, so the check does not itself trigger the load:

after bootstrap init after first read wp_get_icon('core/arrow-left')
trunk 88 icons 88 icons valid SVG
this PR 0 icons 88 icons valid SVG

Note for the committer

The new tests carry @ticket 00000 placeholders — these need the real Trac ticket number before landing.

Context

From a 7.0.4 → 7.1 RC3 benchmark comparison showing a server-side regression on both Block and Classic themes (Block wp-total p50 +9.88% / +10.53 ms; Classic +7.92% / +3.99 ms), with LCP − TTFB flat on both — i.e. PHP execution time, not front-end or database. This was one of the fixed per-request costs identified; at 0.221 ms it is roughly 43% of the Classic theme's entire wp-before-template regression.

An alternative approach — moving _x() out of the generated manifest and translating lazily at read time — was considered and rejected. The manifest is copied verbatim from gutenberg/packages/icons/src/manifest.php by a Gruntfile.js task, and those literal _x() calls are what the i18n tooling extracts. Turning them into plain data would silently drop all 88 strings from core's POT, and there is no icon equivalent of block-i18n.json / theme-i18n.json to declare them. Deferring the include keeps the _x() literals intact and avoids the problem entirely.

`_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) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant