[wp-trac] [WordPress Trac] #55344: Resources for hidden widgets are loaded on WP dashboard
WordPress Trac
noreply at wordpress.org
Wed Apr 8 08:35:32 UTC 2026
#55344: Resources for hidden widgets are loaded on WP dashboard
-----------------------------+-----------------------------
Reporter: josklever | Owner: (none)
Type: feature request | Status: assigned
Priority: normal | Milestone: Future Release
Component: Administration | Version:
Severity: normal | Resolution:
Keywords: needs-patch | Focuses: javascript
-----------------------------+-----------------------------
Comment (by sanket.parmar):
Thanks for the report and to everyone who weighed in. The problem is real
and fixable without a major architectural change. This comment proposes a
concrete approach.
----
== How the current system works ==
The Screen Options toggle is fully client-side: `postbox.js` calls
`$postbox.hide()` or `$postbox.show()`, then persists the new visibility
state via an AJAX call to `wp-ajax.php?action=closed-postboxes`. The
server stores the hidden widget IDs in `metaboxhidden_dashboard` user meta
— ''without'' a page reload.
On the next full page load, `get_hidden_meta_boxes( $screen )` reads that
user meta and returns the list of hidden widget IDs. This list is already
used server-side (e.g. to render the Screen Options checkboxes), but is
'''never consulted before plugins enqueue their assets'''.
The typical plugin pattern looks like this:
{{{#!php
function my_plugin_dashboard_setup() {
wp_enqueue_script( 'my-plugin-dashboard' ); // ← always runs, hidden
or not
wp_add_dashboard_widget( 'my_plugin_widget', 'My Widget',
'my_plugin_widget_render' );
}
add_action( 'wp_dashboard_setup', 'my_plugin_dashboard_setup' );
}}}
There is no supported way for a plugin to say "only enqueue this if my
widget is visible." The bug exists in core itself too — the Site Health
widget is the only core widget that explicitly enqueues separate assets,
and it does so unconditionally — but the primary audience for a fix is
plugin authors.
----
== UX constraint ==
If we skip enqueueing scripts for a hidden widget, and the user then un-
hides it via Screen Options ''without a page reload'', the widget's JS
will be absent until the next full load.
This is the same trade-off that applies to all other meta-box screens in
WordPress. A user toggling Screen Options on any post edit screen faces
the same behaviour today. [comment:7 Comment #7] on this ticket already
acknowledges this, and it is the accepted outcome — '''no additional UX
change or page-reload prompt is needed'''.
----
== Distinguishing hidden vs. closed ==
Two separate user-meta keys exist:
* `metaboxhidden_dashboard` — widget unchecked in Screen Options →
removed from the DOM entirely
* `closedpostboxes_dashboard` — widget collapsed (title bar only, content
still in DOM)
The fix should apply '''only''' to hidden widgets. Collapsed widgets are
still present in the DOM and their scripts may be needed when the user
expands them without a page reload. `get_hidden_meta_boxes()` returns
exactly the hidden list.
----
== Proposed approaches ==
=== Approach A — New `$enqueue_callback` parameter on
`wp_add_dashboard_widget()` (recommended) ===
Add an optional 8th parameter. The function checks the hidden list once
and only invokes the callback when the widget is visible. All callers —
core and plugins — can opt in with a single-line change.
{{{#!php
/**
* @since X.X.0
*
* @param callable|null $enqueue_callback Optional. Callback that enqueues
* scripts/styles for this widget. Only called when the widget is not
* hidden via Screen Options. Default null.
*/
function wp_add_dashboard_widget(
$widget_id,
$widget_name,
$callback,
$control_callback = null,
$callback_args = null,
$context = 'normal',
$priority = 'core',
$enqueue_callback = null // ← new, backward-compatible
) {
global $wp_dashboard_control_callbacks;
$screen = get_current_screen();
if ( is_callable( $enqueue_callback ) ) {
$hidden = get_hidden_meta_boxes( $screen );
if ( ! in_array( $widget_id, $hidden, true ) ) {
call_user_func( $enqueue_callback );
}
}
// ... rest of existing function unchanged ...
}
}}}
Plugin migration is minimal:
{{{#!php
// Before:
function my_plugin_dashboard_setup() {
wp_enqueue_script( 'my-plugin-dashboard' );
wp_add_dashboard_widget( 'my_plugin_widget', 'My Widget',
'my_plugin_widget_render' );
}
// After:
function my_plugin_dashboard_setup() {
wp_add_dashboard_widget(
'my_plugin_widget',
'My Widget',
'my_plugin_widget_render',
null, null, 'normal', 'core',
function() {
wp_enqueue_script( 'my-plugin-dashboard' );
}
);
}
}}}
'''Pros:''' Centralised, consistent, documented, backward-compatible
(default `null` means existing calls are untouched).[[BR]]
'''Cons:''' Requires plugin authors to update their code to benefit; does
not automatically fix existing plugins.
----
=== Approach B — Dynamic action hook ===
Instead of a new parameter, fire a targeted action inside
`wp_add_dashboard_widget()` only when the widget is visible:
{{{#!php
if ( ! in_array( $widget_id, get_hidden_meta_boxes( $screen ), true ) ) {
/**
* Fires when a dashboard widget's resources should be enqueued.
* Only fires if the widget is not hidden via Screen Options.
*
* @since X.X.0
*
* @param string $widget_id The widget ID.
*/
do_action( "wp_dashboard_widget_enqueue_{$widget_id}", $widget_id );
}
}}}
Plugin usage:
{{{#!php
add_action( 'wp_dashboard_widget_enqueue_my_plugin_widget', function() {
wp_enqueue_script( 'my-plugin-dashboard' );
} );
}}}
'''Pros:''' Hook-based; no new function signature; trivial for plugins to
adopt.[[BR]]
'''Cons:''' Dynamic hook name is harder to discover and grep for; does not
help if enqueue calls happen before `wp_add_dashboard_widget()` is called.
----
== What must not break ==
||= Area =||= Concern =||= Safe? =||
|| First visit (no user pref set) || `get_hidden_meta_boxes()` returns
`[]` by default for the dashboard screen → all resources enqueued as today
|| ✓ ||
|| Toggle hide → show without reload || Scripts absent until next full
load — same as all other meta-box screens || ✓ (accepted) ||
|| Collapsed (closed) widgets || `closedpostboxes_dashboard` ≠
`metaboxhidden_dashboard`; collapsed widget scripts correctly loaded || ✓
||
|| Existing 7-argument callers of `wp_add_dashboard_widget()` || New 8th
param defaults to `null`; all existing calls unchanged || ✓ ||
|| `wp_dashboard_setup` action callbacks that enqueue before calling
`wp_add_dashboard_widget()` || Not automatically fixed; plugin authors
need to move enqueue into the callback || ✗ (known limitation, same for
Approach B) ||
|| Multisite per-user prefs || `metaboxhidden_dashboard` is per-user-per-
blog; `get_hidden_meta_boxes()` already handles this || ✓ ||
|| Widget ordering / drag-drop || Sortable postboxes do not affect the
hidden meta || ✓ ||
----
== Tests needed ==
File: `tests/phpunit/tests/admin/includesTemplate.php` (alongside the
existing `test_wp_add_dashboard_widget`)
1.
`test_wp_add_dashboard_widget_enqueue_callback_called_when_visible`[[BR]]Widget
ID not in `metaboxhidden_dashboard`. Assert the enqueue callback is
invoked.
2.
`test_wp_add_dashboard_widget_enqueue_callback_not_called_when_hidden`[[BR]]Add
the widget ID to `metaboxhidden_dashboard` user meta. Assert the enqueue
callback is '''not''' invoked.
3.
`test_wp_add_dashboard_widget_enqueue_callback_not_called_when_closed_but_not_hidden`[[BR]]Add
the widget ID to `closedpostboxes_dashboard` but '''not'''
`metaboxhidden_dashboard`. Assert the callback IS invoked (collapsed ≠
hidden).
4.
`test_wp_add_dashboard_widget_null_enqueue_callback_no_error`[[BR]]Call
with default `null` callback; assert no error is produced (backward-
compatibility regression test).
5.
`test_wp_add_dashboard_widget_enqueue_callback_called_when_no_user_pref`[[BR]]Delete
`metaboxhidden_dashboard` user option entirely (first-visit scenario).
Assert callback IS invoked.
----
== Recommendation ==
'''Approach A''' (`$enqueue_callback` parameter) is the better long-term
API — it keeps the enqueue logic co-located with the widget registration,
is self-documenting, and is easy to validate in tests. Approach B is
simpler to adopt but less discoverable.
Happy to put together a patch for either approach.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/55344#comment:13>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list