JS and CSS coding with a cup of coffeeenqueue-js-css-wtc

Most themes enqueue everything on every page. The syntax highlighter for code samples loads on the contact page, the slider script loads on posts without a slider, and the form styles load where there’s no form. Each file is small, but together they’re what makes a page slower than it needs to be.

The fix is a simple rule: register every asset once, and enqueue it only where it’s used. WordPress already has the tools for it, both in classic templates and in the block editor.

Register once, by name

Registering tells WordPress an asset exists without loading it. Do it once, early, and give the CSS and JavaScript the same handle:

functions.phpPHP
add_action( 'init', function () {
    $assets = get_theme_file_uri( 'assets/' );

    wp_register_style( 'prism', $assets . 'prism/prism.css', array(), '1.1' );

    wp_register_script(
        'prism',
        $assets . 'prism/prism.js',
        array(),
        '1.1',
        array(
            'in_footer' => true,
            'strategy'  => 'defer',
        )
    );
} );

function wptc_load_assets( $handles ) {
    foreach ( (array) $handles as $handle ) {
        if ( wp_style_is( $handle, 'registered' ) ) {
            wp_enqueue_style( $handle );
        }

        if ( wp_script_is( $handle, 'registered' ) ) {
            wp_enqueue_script( $handle );
        }
    }
}
  • One list of assets. Every file the theme can load is registered in one place, with its version and dependencies.
  • One call to load it. wptc_load_assets() enqueues whatever is registered under each name, the stylesheet, the script or both, and quietly skips names it doesn’t know.
  • WordPress handles the rest. A handle is only printed once, however many templates ask for it, and dependencies load in the right order.

Ask for assets in the template

Each template now asks for what it needs. A recipe template with code samples asks for the syntax highlighter, and no other template does:

single-recipe.phpPHP
<?php
wptc_load_assets( array( 'prism' ) );

get_header();

Call it before get_header(). The stylesheet is then queued before wp_head runs, so it prints in the <head> like any other stylesheet, and the script follows in the footer.

You can also decide from functions.php instead of the template, using a conditional tag:

functions.phpPHP
add_action( 'wp_enqueue_scripts', function () {
    if ( is_singular( 'recipe' ) ) {
        wptc_load_assets( array( 'prism' ) );
    }
} );

Both do the same job. The template call keeps each template’s assets next to the markup that needs them; the conditional keeps every decision in one file.

When the call comes later

Sometimes the decision can only be made halfway down the page, for example inside a template part that only some pages include. Where the CSS ends up then depends on the WordPress version:

Where you enqueue Where the CSS prints
Before get_header(), or on wp_enqueue_scripts In the <head>, on every version
After wp_head, on WordPress 6.9 or later in a classic theme Moved up into the <head> by WordPress before the page is sent
After wp_head, on earlier versions In the footer, so the content can briefly show unstyled

WordPress 6.9 added this for classic themes: it buffers the page, and any stylesheet enqueued after wp_head is moved up into the head before the HTML is sent. It’s on by default, but a plugin can turn the buffer off, so asking early is still the safest habit.

In the block editor, blocks do it for you

Blocks carry their own assets, so you don’t write the conditional at all. Declare the files in block.json, and WordPress loads them only on pages where the block appears:

block.jsonJSON
{
    "name": "wptc/tabs",
    "title": "Tabs",
    "style": "file:./style.css",
    "viewScript": "file:./view.js",
    "render": "file:./render.php"
}
  • style loads on the front end and in the editor.
  • viewScript loads on the front end only, and only where the block is used.
  • editorScript and editorStyle load in the editor only, so visitors never download them.

Block themes already load block assets this way. Since WordPress 6.9, classic themes load block styles on demand too, so a theme that mixes PHP templates and blocks gets the same benefit.

Assets for core blocks

Sometimes the asset belongs to a block you didn’t write, like a syntax highlighter for the core Code block. wp_enqueue_block_style() attaches a stylesheet to any block, and a render_block filter adds the script when the block renders:

functions.phpPHP
add_action( 'init', function () {
    wp_enqueue_block_style(
        'core/code',
        array(
            'handle' => 'prism',
            'src'    => get_theme_file_uri( 'assets/prism/prism.css' ),
            'path'   => get_theme_file_path( 'assets/prism/prism.css' ),
            'ver'    => '1.1',
        )
    );
} );

add_filter( 'render_block_core/code', function ( $block_content ) {
    wp_enqueue_script( 'prism' );

    return $block_content;
} );

Now the highlighter loads on any page with a Code block, whether it’s a recipe, a Brew Note or a page, and nowhere else. Nobody has to remember to ask for it. This relies on block styles loading on demand, which block themes do and classic themes do from 6.9; where it’s turned off, WordPress loads the stylesheet on every page instead.

Which method to use

The asset belongs to Load it with
A PHP template wptc_load_assets() before get_header(), or a conditional on wp_enqueue_scripts
Your own block style and viewScript in its block.json
A core or third-party block wp_enqueue_block_style(), plus a render_block_{name} filter for scripts
Every page A plain enqueue on wp_enqueue_scripts, and keep this list short

Check it worked

  1. Open a page that should use the asset, open DevTools, and filter the Network tab by its name. It should load once.
  2. Open a page that shouldn’t use it and do the same. There should be no request at all.
  3. View the page source and confirm the stylesheet’s <link> is inside the <head>, not near </body>.

Before you ship

  • Every asset is registered once, with a version, on init.
  • Templates ask for their assets before get_header(), or a conditional asks for them on wp_enqueue_scripts.
  • Block assets are declared in block.json, not enqueued by hand.
  • Assets for core blocks use wp_enqueue_block_style() and a render_block filter.
  • The list of assets loaded on every page is short, and each one earns its place.

If the page doesn’t use it, the page doesn’t load it. The house rule