WordPress.org

Make WordPress Core

Opened 5 months ago

Last modified 5 months ago

#47285 new feature request

Better Management of External Asset Dependencies

Reported by: justlevine Owned by:
Milestone: Awaiting Review Priority: normal
Severity: major Version:
Component: Script Loader Keywords:
Focuses: javascript, performance Cc:
PR Number:

Description

Tl;Dr: there must be a way for WordPress Plugins to indicate compatible versions of external dependencies, so as to reduce enqueuing multiple copies of the same external library. The issue is urgent with the ecosystem's migration to Blocks.

Problem
We've all been there; you install 3 plugins, and your website is enqueuing four copies of FontAwesome, and two copies of Bootstrap. Plus an extra one of each from your theme. Plus 3 different datepicker libraries and two <select> enhancements.

For most end-users, the buck stops there a their websites are left wasting precious resources loading things they don't need, further proof for detractors that WordPress and Performance are contradictory. For tech-savvy users and developers the headache just begins, and they begin to dequeue assets by trial-and-error, making sure everything is still compatible. And again each time anything updates.

Then there's the impending nightmare scenario: with the move from encompassing plugins to individual functionality Blocks, and the encapsulated dependencies each one will inevitably need to include to function autonomously (especially if/when the new Block Installer kills the need for All-in-One block plugins), the number of unnecessary, duplicate dependencies is about to grow exponentially.

Solution
WordPress needs to allow developers to define what external libraries and compatible versions their code requires, so it can automatically load only one copy of the necessary assets.

Proposal
Warning: I'm nowhere close to being a DevOps expert. While I stand by the issue and generic solution, there's a 95% chance that the the proposal is ineffecient at best, and inherently flawed at worse. This goes doubly for any functions I'm making up on the spot.

1. Allow users to semantically define what external dependencies they're using.

While WordPress is smart enough not to load identical asset handles, those handles are semantically meaningless. It's bad practice to give your external asset a generic name (there are times where you need a specific version or copy of an asset, else you'll have conflicts and incompatibilities), you currently end up with plugin-bootstrap, my-bootstrap, bootstrap-four, and bootstrap, with no way to know that 2 of them are the exact same file, and the 3rd one's source will work perfectly with that version.

A function, such as wp_define_asset_library($handle, $library_name), would let WordPress know that all four files are actually Bootstrap, and you probably need to enqueue only one one them.

Want to use FontAwesome Pro instead of the free version? You could then simply dequeue all of them with the $library_name, instead of trying to figure out what each plugin author decided to call the one they bundled.

Bonus: Throw in a conditional is_library_registered($library_name), and Block Authors could support both jQuery.Datepicker and PickaDate, and you wouldn't need to be running two JS libraries that do the same thing just because you're using blocks from two different authors! Hashtag Open Ecosystem.

Note: a standard naming convention for dependency names would need to be agreed upon. I'd suggest using the official repository name, but as I said in my disclaimer, I don't know squat.

2. Let Developers define supported versions of the library.

By giving a extra argument to our function above, and we've solved the headache of version conflicts!

Defining $vers, WordPress will be able to look at all 3 different versions of bootstrap and automagically know which one to include! Plugin A requires 4.1.1, Plugin B ^4.0, and Plugin C is still stuck with ~3.0.5, and suddenly you've been able to drop a redundant library but your legacy plugin is still working fine!

Imagine a world where you could use jQuery 3.4 without worrying about WordPress' precious back-compat. Heck, imagine being confident enough to get rid of jQuery as a frontend dependency althogether, because you know for certain only one plugin is using it, and it offers a vanilla JS alternative (via the conditional in #1)!

Bonus: If two versions need to be included, WordPress could even tell the plugin to 'auto-encapsulate' one of them! Say goodbye to manual 'No Conflict Modes' and hours of plugin support time wasted telling end-users "Our plugin is incompatible with one of your other installed plugins. Disable them one-by-one until you find the culprit, and then bet it's developer to update it's version of select2", and hello to increased interoperability!

Note: suggested practice would be for the developer to include the most up-to-date version compatible with their plugin, while declaring compatibility for earlier versions. WordPress would would then include the most-recent version(s) from whichever source meets the criteria.

Implementation & Backwards Compatibility
Again, I'm no expert, but from my utterly uninformed point of view, this is easy to implement both without breaking backwards compatibility, and possible improving it going forward.

wp_register_script and wp_register_style get one extra parameter: $library_name. $ver gets a check to see if it's a number or the min/max version syntax. If we're worried about human readability then you put $library_name as the fifth argument and if it's a boolean you treat it as the old in footer.

Ditto for if you use wp_enqueue_whichever to register the asset when enqueuing. If the script is already registered, it checks whether there's an associated library before doing the enqueue process (below).

A new function wp_define_asset_library($handle, $library_name, $ver) (yeah this shouldn't be the actual name) lets either the plugin developer, or more importantly the user, easily associate any external assets declared the old way with an external library.

The $wp_scripts global, essentially stays the same but with the new array value. I'm not a performance expert but maybe another global $wp_script_libraries contains a multidimensional array of $library_names, with the defined handle and version of each.

The enqueue process checks if $library_name is defined, and if so begins the automagical process of figuring out which asset to enqueue:
-parses the version requirements for all the same libraries.
-If several are in the same range it enqueues the highest compatible version version.
-any versions that aren't in a range, and any assets that don't have an external library are enqueued normally.

wp_dequeue/deregister_style_library($library_name, $versions) and wp_dequeue_script_library, gets rid of all associated assets in that range.

is_library_registered($handle, $vers) let's you do cool things like support multiple libraries based on what other plugins, or choose whether to to register an asset altogether (e.g. use jQuery.Datepicker if it - or jQuery is enqueued, else register a vanilla JS version).

Similarly, $is_enqueued_multiple_times(handle), let's authors prepend a $noconflict_prefix when necessary.


Again, I'm sure there's a bunch of unseen issues with this, but end of the day the switch to Blocks (and especially the push for individual blocks decoupled from a larger plugin) is going to bring around an external library apocalypse.

We need to find and implement a solution, and soon!

Change History (4)

#1 follow-up: @nerrad
5 months ago

Worth noting, this problem is not new :) In the case of Gutenberg and blocks, there is some awareness around dependencies management which in part led to the creation of this package: https://github.com/WordPress/gutenberg/tree/master/packages/dependency-extraction-webpack-plugin

It's the first step in hopefully improving how assets get registered within the block-editor (and eventually WordPress) environment. Also, as a part of the block rfc (https://github.com/WordPress/gutenberg/pull/13693) there's some discussion around declaration of assets as aa part of a block.json.

The above doesn't speak to all your points in here but highlights related conversations/solutions happening elsewhere.

#2 @swissspidy
5 months ago

Worth noting that the theme directory has some requirements for theme developers to always use the same script handle for external libraries when they use wp_register_script(). That already solves many issues.

Defining $vers, WordPress will be able to look at all 3 different versions of bootstrap and automagically know which one to include! Plugin A requires 4.1.1, Plugin B 4.0, and Plugin C is still stuck with ~3.0.5, and suddenly you've been able to drop a redundant library but your legacy plugin is still working fine!

Resolving dependencies is not an easy task like this. Especially when not all libraries can be easily loaded multiple times. https://journal.rmccue.io/322/plugin-dependencies/ is a good article about how that works. Different area, but similar problems.

#4 in reply to: ↑ 1 @justlevine
5 months ago

Replying to nerrad:

Worth noting, this problem is not new :)

True that :) My big fear for why is believe it's now more urgent than ever is my understanding of the Block Library / Installer. When everything needs to be included in a standalone block, larger-scope plugins and all-in-one collections (where the author is using one version of one specific library for one function) become obsolete and replaced with everyone using the library@version they like the most. Not just do things become harder to dequeue, but you also end up loading even more competing libraries that a simple dequeue won't solve.

In the case of Gutenberg and blocks, there is some awareness around dependencies management

Am I totally misunderstanding, or is stuff there more regarding handling core WP libraries, and build processes? Which is obviously would be an even bigger issue (and I'm getting anxious just thinking about it)!

I've only really seen a few slack messages or GitHub comments discussing the inclusion of external (non-Core) libraries, and how to prevent the end user from loading the 20 copies of FontAwesome in this distopian near-future. It'd be a relief to know this is on the radar of people who are actually qualified to discuss it!

The above doesn't speak to all your points in here but highlights related conversations/solutions happening elsewhere.

Thanks! As I said an umpteen amount of times, while the issue is pretty obvious, my solution is half-assed and will hopefully (solely) serve to get this on the radar of people who aren't fretting about the #assetApocalypse, and the people who can actually do something to prevent it!

Replying to swisspidy:

Worth noting that the theme directory has some requirements for theme developers to always use the same script handle for external libraries when they use wp_register_script().

If I'm not mistaken, it's only 'recommended'. More importantly this definitely isn't required for plugins, and to my knowledge is mostly discouraged in the developer community so as to prevent errors from using an incompatible library version. Which makes sense, IMO; better to include two scripts than increase the likelihood that the '80%' non-dev-enduser won't be able to get the plugin to work.
But with mixing-and-matching functionality blocks on its way to becoming the norm, neither relying on handles nor including multiple copies of a dependency will be sustainable (comparative to now).

Resolving dependencies is not an easy task like this.

I believe you. I'm admittedly more than wholly unqualified to expect that the specifics of any part of the solution I proposed are sound. 😊

Can you elaborate a bit though?

From my basic understanding there's two issues when dequeuing and/or replacing multiple copies of a library:

  • The two versions aren't compatible. This is solved (well enough) by semver. If a plugin author says their plugin is compatible with 4.x.x, then even if they include v4.2, I can safely use 4.1, or the future-releaded 4.4. if someone is using v3 or v5, it gets included separately.
  • Two versions of the library can't be run at the same time. Sometimes this can be resolved with prefixing, or a noConflict mode included in the library. Other times, you're out of luck. Either way, this problem already exists, but with this proposal can be somewhat mitigated (by enqueueing a single version that meets both plugin requirements, or with WP core conditionals that check whether the library is enqueued twice and make it easier for either the plugin author or site developer to address the issue).

And of course, there's likely a better way to resolve the dependencies, that someone more experienced than I would contribute!

Note: See TracTickets for help on using tickets.