Skip to content

Conversation

@b1ink0
Copy link

@b1ink0 b1ink0 commented Sep 13, 2025

This pull request introduces support for printing script modules in the footer.

Trac ticket: https://core.trac.wordpress.org/ticket/63486

Companion Gutenberg PR for upstream changes: WordPress/gutenberg#72459

TODO:

  • Remove PHPStan annotations.
  • Print modulepreload links for footer script modules also in the footer. (Maybelater)

This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

Hi @b1ink0! 👋

Thank you for your contribution to WordPress! 💖

It looks like this is your first pull request to wordpress-develop. Here are a few things to be aware of that may help you out!

No one monitors this repository for new pull requests. Pull requests must be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description.

Pull requests are never merged on GitHub. The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making.

More information about how GitHub pull requests can be used to contribute to WordPress can be found in the Core Handbook.

Please include automated tests. Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the Automated Testing page in the handbook.

If you have not had a chance, please review the Contribute with Code page in the WordPress Core Handbook.

The Developer Hub also documents the various coding standards that are followed:

Thank you,
The WordPress Project

@github-actions
Copy link

github-actions bot commented Sep 13, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props b1ink0, westonruter, jonsurrell, mindctrl, peterwilsoncc.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@b1ink0 b1ink0 requested a review from westonruter September 29, 2025 13:06
Comment on lines 308 to 314
// If any dependency is set to be printed in footer, skip printing this module in head.
$dependencies = $this->get_dependencies( array( $id ) );
foreach ( $dependencies as $dependency ) {
if ( $dependency['enqueue'] && $dependency['in_footer'] ) {
continue 2;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right? Should they not both be printed in the head instead?

Consider classic scripts:

add_action( 'wp_enqueue_scripts', function () {
	wp_enqueue_script( 'foo-footer', '/footer.js', array(), false, array( 'in_footer' => true ) );
	wp_enqueue_script( 'bar-head', '/head.js', array( 'foo-footer' ), false, array( 'in_footer' => false ) );
} );

This results in the following being printed in the head:

<script src="http://localhost:8000/footer.js?ver=6.9-alpha-60093-src" id="foo-footer-js"></script>
<script src="http://localhost:8000/head.js?ver=6.9-alpha-60093-src" id="bar-head-js"></script>

Nevertheless, for script modules since they are always deferred to execute until after the DOM has loaded, whether they are in the head or footer makes little difference.

I just want to make sure that we're intentional about this. Or if it makes more sense to align the behavior with classic scripts.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there is a head module which depends on a footer module? This is accounted for in WP_Scripts.

#9867 (comment)

I think I made a mistake the current logic is the reverse of what happens with classic scripts.

The only difference when script modules are in the footer is that the download starts later compared to those in the head, but I'm not sure if that's good or bad. If we don't care about the later download, then it makes sense to align the behavior with classic scripts.

Is there any benefit to moving the dependent to the footer if its dependency is in the footer for script modules that I might be missing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a similar question to #9770 with fetchpriority. If foo has a has a high priority, and its dependency bar has a low priority. What is being sorted out there is whether the priority of bar establishes the priority of foo when it is enqueued (if foo was not directly enqueued as well). So if bar is enqueued, then a modulepreload link for foo is added with fetchpriority=low because bar is low.

In the same way, let's say you register a script module bar to be printed in the HEAD, but it has a dependency foo which is supposed to print in the footer. If you just enqueue bar, then a modulepreload link for foo could be added to the HEAD along with the SCRIPT for bar.

However, if you enqueue both foo and bar, then we have to figure out whether to move bar to the the footer to go along with foo, or we move foo to go into the HEAD along with bar. The order matters because it's possible they are not behaving as pure modules and could be interacting with globals.

In reality, this is probably very much an edge case that we shouldn't stress about. Since script modules do not execute until the DOM has loaded anyway, it doesn't really matter where they are located from a functional perspective (ahem, unless async is added in which they might, but we support these, and the order is even less important in this case).

cc @sirreal for his thoughts

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested and thought about this a lot. I think the safest thing should be done, which is that later wins. If a module in the head depends on a module in the footer, they should move to the footer. If a module in the footer depends on a module in the head, it doesn't matter they can both stay where they declared.

This comes from an assumption that it's more likely for modules to expect DOM to be loaded than to expect the DOM not to be loaded. It's also closer to the standard non-async behavior of modules, in case an async module imports a non-async module that expect to load after the DOM.

less organized, rambling thoughts

The order matters because it's possible they are not behaving as pure modules and could be interacting with globals.

It seems practically malicious for modules to rely on a particular script tag ordering. I'm reluctant even consider that as behavior to support. I've managed to create an error situation like is described here, but it wasn't easy. The only way that occurred to me to trigger this problem was to put an async module at the top of a document that imported a non-async module that expected to load after the DOM. This already suggests what the solution should be (later wins).

I'll share some assumptions and beliefs I have about modules:

  • Modules always load after the DOM unless they're async.
  • Modules are easy to reason about unless they're async.
  • Modules in WordPress are not async. It's possible to make them async, but by default they are not.
  • Trying to anticipate and support the mess that async modules can make is likely to be frustrating and may ultimately be impossible.
  • Dependency and enqueued modules tend to be different.
  • Enqueued modules are entry points that do things on their own, they have side effects of booting up functionality when they're evaluated. Usually this is via a <script> tag, but they can be imported.
  • It's unusual for enqueued modules to export things, so it's unusual for enqueued modules to depend on other enqueued modules. A possible exception is when a module depends on the side effect of another module.
  • Dependency modules can and will have side effects. These may expected to load after the DOM.

@b1ink0 b1ink0 requested a review from westonruter October 9, 2025 16:24
@westonruter
Copy link
Member

@b1ink0 sorry for the delay. I had to commit two prerequisite changes: 9d03e8e and fe9b235. Unfortunately these mean there are now some merge conflicts. Could you resolve?

@b1ink0
Copy link
Author

b1ink0 commented Oct 14, 2025

@b1ink0 sorry for the delay. I had to commit two prerequisite changes: 9d03e8e and fe9b235. Unfortunately these mean there are now some merge conflicts. Could you resolve?

Yes will do it.

@b1ink0
Copy link
Author

b1ink0 commented Oct 14, 2025

Merge conflict resolved.
cc: @westonruter

@b1ink0 b1ink0 requested a review from sirreal October 14, 2025 20:34
…into enhancement/63486-script-modules-footer-support
@b1ink0
Copy link
Author

b1ink0 commented Oct 20, 2025

I checked classic scripts as well, and they also cause the same memory exhaustion error for circular dependencies but in the WP_Dependencies class:

@b1ink0 So then it seems we're ok here?

Yes, I think so. I just wanted to highlight whether we want to handle this case for the script module.

Copy link
Contributor

@peterwilsoncc peterwilsoncc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good, I'm about to do some manual testing.

In the new and modified tests, are you able to add an @ticket 63486 annotation for ease of testing at a later date?

westonruter and others added 5 commits October 20, 2025 16:43
Co-authored-by: Peter Wilson <[email protected]>
…into enhancement/63486-script-modules-footer-support
…tion API in some plugins

This reverts commit 9915940.

Co-authored-by: Peter Wilson <[email protected]>
westonruter and others added 3 commits October 20, 2025 17:28
Copy link
Contributor

@peterwilsoncc peterwilsoncc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm done with my various nitpicks and notes and it's working as expected based on my manual testing.

👍

pento pushed a commit that referenced this pull request Oct 21, 2025
This brings API parity with `WP_Scripts` by implementing opt-in support for printing in the footer via an `in_footer` argument. This argument can be supplied via the `$args` array passed to `wp_enqueue_script_module()` or `wp_register_script_module()`, alongside the existing `fetchpriority` key introduced in #61734. It can also be set for previously-registered script modules via `WP_Script_Modules::set_in_footer()`. This is not applicable to classic themes since modules are enqueued while blocks are rendered after `wp_head` has completed, so all script modules are printed in the footer anyway; the `importmap` script must be printed after all script modules have been enqueued.

Script modules used for interactive blocks (with the Interactivity API) are automatically printed in the footer. Such script modules should be deprioritized because they are not in the critical rendering path due to interactive blocks using server-side rendering. Script modules remain printed at `wp_head` by default, although this default should be revisited since they have deferred execution (and they are printed in the footer for classic themes already, as previously noted). Moving a script module to the footer ensures that its loading does not contend with the loading of critical resources, such as the LCP element's image resource, and LCP is improved as a result.

This also improves specificity of some PHP types, it ensures that script modules can't be registered with an empty ID, and it prevents printing script modules with empty `src` URLs.

Developed in #9867

Follow-up to [60704].

Props b1ink0, westonruter, jonsurrell, peterwilsoncc, vipulpatil, mindctrl.
See #61734.
Fixes #63486.


git-svn-id: https://develop.svn.wordpress.org/trunk@60999 602fd350-edb4-49c9-b593-d223f7449a82
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Oct 21, 2025
This brings API parity with `WP_Scripts` by implementing opt-in support for printing in the footer via an `in_footer` argument. This argument can be supplied via the `$args` array passed to `wp_enqueue_script_module()` or `wp_register_script_module()`, alongside the existing `fetchpriority` key introduced in #61734. It can also be set for previously-registered script modules via `WP_Script_Modules::set_in_footer()`. This is not applicable to classic themes since modules are enqueued while blocks are rendered after `wp_head` has completed, so all script modules are printed in the footer anyway; the `importmap` script must be printed after all script modules have been enqueued.

Script modules used for interactive blocks (with the Interactivity API) are automatically printed in the footer. Such script modules should be deprioritized because they are not in the critical rendering path due to interactive blocks using server-side rendering. Script modules remain printed at `wp_head` by default, although this default should be revisited since they have deferred execution (and they are printed in the footer for classic themes already, as previously noted). Moving a script module to the footer ensures that its loading does not contend with the loading of critical resources, such as the LCP element's image resource, and LCP is improved as a result.

This also improves specificity of some PHP types, it ensures that script modules can't be registered with an empty ID, and it prevents printing script modules with empty `src` URLs.

Developed in WordPress/wordpress-develop#9867

Follow-up to [60704].

Props b1ink0, westonruter, jonsurrell, peterwilsoncc, vipulpatil, mindctrl.
See #61734.
Fixes #63486.

Built from https://develop.svn.wordpress.org/trunk@60999


git-svn-id: http://core.svn.wordpress.org/trunk@60335 1a063a9b-81f0-0310-95a4-ce76da25c4cd
@github-actions
Copy link

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 60999
GitHub commit: 25420f0

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions bot closed this Oct 21, 2025
github-actions bot pushed a commit to platformsh/wordpress-performance that referenced this pull request Oct 21, 2025
This brings API parity with `WP_Scripts` by implementing opt-in support for printing in the footer via an `in_footer` argument. This argument can be supplied via the `$args` array passed to `wp_enqueue_script_module()` or `wp_register_script_module()`, alongside the existing `fetchpriority` key introduced in #61734. It can also be set for previously-registered script modules via `WP_Script_Modules::set_in_footer()`. This is not applicable to classic themes since modules are enqueued while blocks are rendered after `wp_head` has completed, so all script modules are printed in the footer anyway; the `importmap` script must be printed after all script modules have been enqueued.

Script modules used for interactive blocks (with the Interactivity API) are automatically printed in the footer. Such script modules should be deprioritized because they are not in the critical rendering path due to interactive blocks using server-side rendering. Script modules remain printed at `wp_head` by default, although this default should be revisited since they have deferred execution (and they are printed in the footer for classic themes already, as previously noted). Moving a script module to the footer ensures that its loading does not contend with the loading of critical resources, such as the LCP element's image resource, and LCP is improved as a result.

This also improves specificity of some PHP types, it ensures that script modules can't be registered with an empty ID, and it prevents printing script modules with empty `src` URLs.

Developed in WordPress/wordpress-develop#9867

Follow-up to [60704].

Props b1ink0, westonruter, jonsurrell, peterwilsoncc, vipulpatil, mindctrl.
See #61734.
Fixes #63486.

Built from https://develop.svn.wordpress.org/trunk@60999


git-svn-id: https://core.svn.wordpress.org/trunk@60335 1a063a9b-81f0-0310-95a4-ce76da25c4cd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants