3

In order to avoid poor performance with multiple <script> tags I do concatenation of scripts regularly and produce one single bundle.min.js JS file and 'jsbundle' identifier.

Problem is, things added subsequently, like plugins, may depend on registered one or more libraries that are present, but packed in generic 'jsbundle'.

Is there a way to inform Wordpress that 'jsbundle' implies, for example 'jquery', 'backbone', ... in order for 1) resource not being loaded twice 2) things not failing because of unfulfilled dependency ?

I've tried with source of wp_register_script, found WP_Scripts() class and tried to "lie" WP about available scripts, yet no luck.

4
  • The right way to tackle this is to bundle your shared libraries in a vendor.js file and require that as a dependency for your bundle.min.js file and other scripts.
    – Alexander
    Commented Oct 24, 2017 at 13:43
  • Hi Alexander. Thanks for comment - I'm not sure I understand? Commented Oct 24, 2017 at 13:46
  • Right now you have all your libraries in your bundle.min.js, am I right? But since other parts of you plugin depend on those libraries, it would be logical to create a separate concatenated bundle containing all your libraries (like jqueryand backbone), and a bundle containing vendor code is called vendor.js by convention in a lot of cases. Your bundle.min.js should be split into a bundle containing third party code and a bundle containing your own code.
    – Alexander
    Commented Oct 24, 2017 at 18:34
  • 2
    @Alexander: How would splitting the bundle up make WP know about what libraries are included in the bundle?
    – janh
    Commented Oct 31, 2017 at 17:27

3 Answers 3

4
+50

To have the JavaScript Libraries not to load since you already created a bundle of them, do the following:

Asumming the following, usual enqueue:

function the_js() {
    wp_enqueue_script('bundle_js', get_template_directory_uri() . '/js/bundle.js', array(), false, false);
}

add_action('wp_enqueue_scripts', 'the_js');

and lets say you have in your bundle the following libraries (listing the handles):

  1. jquery
  2. backbone
  3. colorpicker
  4. bootstrap_js

1,2,3 are already in core, 4 is a third party, you bundled all 4 because you dont want the 4 to be loaded as separate resources.

You have to deregister (if they are registered, core ones would be already) and register each one of them, each one of the libraries that are in your bundle:

function the_js() {
    wp_enqueue_script('bundle_js', get_template_directory_uri() . '/js/bundle.js', array(), false, false);

    //DEREGISTER the SCRIPTS THAT ARE IN YOUR BUNDLE
    wp_deregister_script('jquery'); //because its a Core-Registered Script
    wp_deregister_script('backbone'); //because its a Core-Registered Script
    wp_deregister_script('colorpicker'); //because its a Core-Registered Script

    //REGISTER THEM THIS TIME USING YOUR BUNDLE AS DEPENDENCY
    wp_register_script('jquery', FALSE, array('bundle_js'), '', FALSE);//THE KEY HERE IS THE SRC BEING FALSE
    wp_register_script('backbone', FALSE, array('bundle_js'), '', FALSE);
    wp_register_script('colorpicker', FALSE, array('bundle_js'), '', FALSE);

}

add_action('wp_enqueue_scripts', 'the_js');

the key here is to set the $src as FALSE so the registered script will be an alias, check this line in the core code:

// A single item may alias a set of items, by having dependencies, but no source.
if ( ! $obj->src ) {
   return true;
}

its what currently jquery does, when putting jquery as a dependency, it doesnt load jquery it loads jquery-core and jquery-migrate, this is the registered object for jquery:

object(_WP_Dependency)#329 (6) {
  ["handle"]=>
  string(6) "jquery"
  ["src"]=>
  bool(false)
  ["deps"]=>
  array(2) {
    [0]=>
    string(11) "jquery-core"
    [1]=>
    string(14) "jquery-migrate"
  }
  ["ver"]=>
  string(6) "1.12.4"
  ["args"]=>
  NULL
  ["extra"]=>
  array(0) {
  }
}

so, bundle_js its going to be loaded when a script has as dependency of any of the libraries (jquery, backbone, colorpicker) and it will be loaded 1 time, since the logic in WP_Dependencies checks if its already in the queue array.

If you want to check if a script is already registered use:

global $wp_scripts;
$wp_scripts->query('jquery'); //jquery as example handle

it will return a WP_dependency object if its registered, false if its not.

Some links for further understanding:
class.wp-dependencies.php
class.wp-scripts.php
functions.wp-scripts.php

0

The solution proposed by Alexander looks reasonable. You can keep all minified dependencies (libs.js in my example) in one file and your scripts in second file like so:

wp_enqueue_script( 'libs', get_template_directory_uri() . '/assets/js/libs.js', [], '1.0.0', true );

// third parameter here will make sure that libs.js is loaded before jsbundle:

wp_enqueue_script( 'jsbundle', get_template_directory_uri() . '/assets/js/jsbundle.min.js', [ 'libs' ], '1.0.0', true );

Even though bundling scripts is a well known practice for optimising page load speed and is a good idea most of the time, it might not always be the most performant solution. I would always consider some other options and decide what works best for particular use case.

CDN

You might wanna use one of available CDN's for your third party libraries to get all advantages that are listed in this answer.

E.g. If you use Google's or Facebook's CDN, there is a big chance, that your visitors already got popular scripts cached in their browsers and wouldn't need to download it again.

In that case, bundling 3rd party scripts takes away this advantage, and the whole bundle needs to be downloaded, even if the user has part of it already saved in browser's cache.

You can easily enqueue scripts from CDN with wp_enqueue_script(), just omit the $ver parameter, 'cause you don't want to refresh the cached script:

wp_enqueue_script( 'jquery', 'https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js', [], null, true );

Conditional tags

The other thing I would consider is registering more scripts and calling them only on pages where they are actually used. This works well especially if you update your scripts often.

If you keep all scripts in a bundle, applying one little changes requires busting cache for the whole script. Sometimes it might be better to keep parts of your scripts separated, so you can change version only for the chunk that you actually edited.

Let's say I use foo.js script on my homepage and there's not much going on there so I don't plan to change it anytime soon, but at the same time I have a complicated bar.js script that I need to maintain and refresh often. In that case it might be better to register scripts separately.

Also, some of your libraries might be used only on subpages that are not visited very often (let's say I use masonry on one less popular subpage), so preloading them on your home page might not be the way to go. In that case I'd do: // register masonry but don't enqueue it just yet wp_register_script( 'masonry', get_template_directory_uri() . '/assets/js/masonry.min.js', [], 1.0, true );

// foo.js is my implementation of masonry.js
wp_register_script( 'foo.js', get_template_directory_uri() . '/assets/js/masonry.min.js', [ 'masonry' ], 1.0, true );

// in bar.js I keep functions used all over my page so I enqueue it everywhere immediately
wp_enqueue_script( 'bar.js', get_template_directory_uri() . '/assets/js/masonry.min.js', [], 1.0, true );

// masonry.js and foo.js can wait until the user reaches my less popular subpage as it won't be needed most of the time
if ( is_post_type_archive( 'unpopular-posts' ) ){
    wp_enqueue_script( 'foo.js' );
}
5
  • 1
    I believe the question is about how to tell WP that "libs" contains libA, libB, libC so when plugins require libA or libB, it won't get loaded twice. Splitting it into "libs.js" and "jsbundle.min.js" won't help with that.
    – janh
    Commented Oct 31, 2017 at 17:33
  • I think the solution addresses that need - once you register libs.js you can call it as dependency to other scripts - wordpress will handle that and load it only once. Commented Oct 31, 2017 at 20:06
  • How will you change Plugin XYZ's dependencies without changing the plugin (which will be overwritten on the next update)?
    – janh
    Commented Oct 31, 2017 at 20:12
  • Two ways to do that: 1. Use wp_deregister_script() to remove script and then register it again with different dependencies link , 2. Access and modify $wp_scripts global variable link Commented Oct 31, 2017 at 20:34
  • If you're already extending $wp_scripts, you can build the functionality for bundle-depencies ;). I understood the question to be about a general solution, not a per-script/plugin hack.
    – janh
    Commented Oct 31, 2017 at 20:38
0

Quick exchange with @janh under my previous answer inspired me to think about different approach. I would still go with minifying all third party libraries to one separate bundle file (bundlejs), and then work with with global $wp_scripts variable (credit for that idea goes to this thread)

It's never going to be fully automated - as Janh noticed, it's impossible to predict how other people would call the scripts in their plugins or themes. In my function I use an array of script names that my bundle file contains. To double the chances of hitting possible duplicates, I added also the support for script file names instead of handles only.

Bear in mind that some admin scripts shouldn't be touched at all (see codex)

Solution is pretty raw, I'm sure it can be perfected in many ways, definitely could use some refactoring and WP cache support. Also I'm not sure how it affects performance in real life case. But it's here for you to get the general idea and maybe some inspiration:

function switch_dependencies_to_bundle(){
    global $wp_scripts;

    /**
     * array of scripts that our bundle file contains - can be either script name or handle name
     * it's never going to be bulletproof tho', as plugin authors can change file names, i.e.
     * include other version of script
     */
    $bundled = [
        'jquery',
        'masonry',
        'masonry-js',
        'masonry.js',
        'masonry.min.js', //we can use file name too
        'backbone',
        'backbone.min.js'
    ];

    $deps_to_remove = [];

    // register our bundle script
    wp_register_script( 'bundlejs', get_template_directory_uri() . '/assets/js/bundle.min.js', [], '1.0.0', true );
    wp_enqueue_script( 'bundlejs' );

    // get registered scripts that our bundle file would duplicate
    foreach( $wp_scripts->registered as $handle => $script ){
        $file_name = substr( $script->src , strrpos( $script->src, "/" ) + 1 );
        if ( in_array( $handle, $bundled, true ) || in_array( $file_name, $bundled, true ) ){
            $deps_to_remove[] = $handle;
        }
    }

    //get rid of redundant scripts with deregister and dequeue.
    //NOTE: does not work for some admin scripts, see codex
    foreach( $deps_to_remove as $script ){
        wp_deregister_script( $script );
        wp_dequeue_script( $script );
    }

    // take care of remaining scripts' dependencies (they wouldn't load with deps missing)
    // add our bundle as dependency, as it contains the same scripts that we just removed
    foreach( $wp_scripts->registered as $script ){
        if ( ! empty( array_intersect( $deps_to_remove, $script->deps ) ) ) {
            $script->deps[] = 'bundlejs';
            $script->deps = array_diff( $script->deps, $deps_to_remove );
        }
    }
}

// load the function with high priority so it kickes in after other plugins registered their scripts
add_action('wp_enqueue_scripts', 'switch_dependencies_to_bundle', 9999);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.