Skip to content

← All notes

30 July 20265 minWordPress / Elementor / Performance

Loading assets conditionally broke the Elementor editor

Only loading a widget’s CSS on pages that use it is obviously right. Doing the check in the wrong place took the editor down with a 500 on save.

A plugin with several Elementor widgets ships several stylesheets. Enqueue them all on every page and you have made every page on the site slower, including the ones using none of them. So the plugin checks which widgets are actually on the page and enqueues only what is needed.

if ( self::page_has_widget( 'cl-perspective-card' ) ) {
    wp_enqueue_style( 'corlayers-card' );
    wp_enqueue_script( 'corlayers-card' );
}

That is correct, and on the front end it works exactly as intended. Then saving a page in the Elementor editor started returning a 500.

What was actually happening

Elementor does not only fire wp_enqueue_scripts when rendering a page for a visitor. It fires it manually inside the editor too, as a way of collecting which assets a page needs. The hook is the same hook, so the plugin ran its check — in an admin request, where the assumptions underneath it no longer hold.

  • page_has_widget() calls is_singular(), which was being called before the main query was set up — a PHP notice, and a check that cannot answer correctly.
  • To find the widgets, it decodes the page’s full Elementor JSON. Once per widget type, and the plugin has several — fifteen decodes of the same large document in a single request.
  • On a page of any size that exhausts the PHP memory limit, and the save returns 500.

None of it is visible from the symptom. The error is a 500 on save, in the editor, on a plugin whose front end is fine — and the code that causes it is an optimisation that only ever meant to skip a stylesheet.

The fix

The check only means anything on a public page load, so that is the only place it should run. Three guards, before any of the expensive work happens:

if ( is_admin() ||
     ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ||
     ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
    return;
}

The wider lesson is about hooks rather than Elementor. wp_enqueue_scripts reads like "when a page is being built for a visitor", and most of the time it is. But a hook is a name other code can call, and page builders in particular call core hooks in contexts they were never documented for. Anything expensive hanging off one needs to state the context it assumes rather than infer it.

A hook is not a promise about context. It is only a promise that something called it.

Building something like this? Let's talk.

Start a project ↗