-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Script Loader: Bump fetchpriority for dependencies to be as high as recursive dependents for scripts and script modules
#9770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…recursive dependents
|
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 Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe 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
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
fetchpriority for dependencies to be as high as recursive dependentsfetchpriority for dependencies to be as high as recursive dependents
fetchpriority for dependencies to be as high as recursive dependentsfetchpriority for dependencies to be as high as recursive dependents for scripts and script modules
|
I've been reviewing and considering this. What this does is find the highest fetch priority in a dependency graph and apply that fetch priority to the entire graph. Am I interpreting the changes correctly? Let's take an example with two enqueues (entry points), ---
config:
theme: base
---
flowchart LR
classDef HI stroke-width:1px, stroke:#ff0000, fill:#ff000022
classDef LO stroke-width:1px, stroke:#0000ff, fill:#0000ff22
classDef ENQUEUE stroke-width:4px
A["A::low"]
A:::LO
B["B"]
C["C"]
E["E"]
D["D::high"]
D:::HI
X["X::high"]
Y["Y"]
Z["Z"]
X:::HI
A --> B --> C --> E & D
A:::ENQUEUE
X --> D & Y --> Z
X:::ENQUEUE
With this PR, the ---
config:
theme: base
---
flowchart LR
classDef HI stroke-width:1px, stroke:#ff0000, fill:#ff000022
classDef LO stroke-width:1px, stroke:#0000ff, fill:#0000ff22
classDef ENQUEUE stroke-width:4px
A:::HI
B:::HI
C:::HI
E:::HI
Y:::HI
Z:::HI
A["A::low"]
B["B"]
C["C"]
E["E"]
D["D::high"]
D:::HI
X["X::high"]
Y["Y"]
Z["Z"]
X:::HI
A --> B --> C --> E & D
A:::ENQUEUE
X --> D & Y --> Z
X:::ENQUEUE
It seems like the fetch priority should be directed from dependent to dependency. In fact, I wonder whether modules that aren't enqueued should have a fetchpriority at all or if fetchpriority should be associated with enqueue, and cascade to dependents. In other words, dependency modules would inherit fetch priority from their dependents. I think our ideal result graph would look like this: ---
config:
theme: base
---
flowchart LR
classDef HI stroke-width:1px, stroke:#ff0000, fill:#ff000022
classDef LO stroke-width:1px, stroke:#0000ff, fill:#0000ff22
classDef ENQUEUE stroke-width:4px
A:::LO
B:::LO
C:::LO
E:::LO
Y:::HI
Z:::HI
A["A::low"]
B["B"]
C["C"]
E["E"]
D["D::high"]
D:::HI
X["X::high"]
Y["Y"]
Z["Z"]
X:::HI
A --> B --> C --> E & D
A:::ENQUEUE
X --> D & Y --> Z
X:::ENQUEUE
|
|
@sirreal I think what you're describing is almost exactly what was implemented. It is indeed being directed from dependent to dependency. This follows the same approach as was implemented by the script loading strategies, where the dependent dictates what loading strategy a dependency is eligible to have. For example, a dependent with a blocking loading strategy cannot have a dependency with a I've added two test cases that show what happens with both classic scripts and script modules in 3dd0e0f, using the dependency graph you came up with. The result is it comes up with the ideal result graph, with the exception that it is also taking into account the The initial graph you depicted actually gets resolved as follows: ---
config:
theme: base
---
flowchart LR
classDef HI stroke-width:1px, stroke:#ff0000, fill:#ff000022
classDef LO stroke-width:1px, stroke:#0000ff, fill:#0000ff22
classDef ENQUEUE stroke-width:4px
A:::LO
Y:::HI
Z:::HI
A["A::low"]
B["B"]
C["C"]
E["E"]
D["D::high"]
D:::HI
X["X::high"]
Y["Y"]
Z["Z"]
X:::HI
A --> B --> C --> E & D
A:::ENQUEUE
X --> D & Y --> Z
X:::ENQUEUE
Classic scripts: <script type="text/javascript" src="/z.js" id="z-js" fetchpriority="high" data-wp-fetchpriority="auto"></script>
<script type="text/javascript" src="/d.js" id="d-js" fetchpriority="high"></script>
<script type="text/javascript" src="/e.js" id="e-js"></script>
<script type="text/javascript" src="/c.js" id="c-js"></script>
<script type="text/javascript" src="/b.js" id="b-js"></script>
<script type="text/javascript" src="/a.js" id="a-js" fetchpriority="low"></script>
<script type="text/javascript" src="/y.js" id="y-js" fetchpriority="high" data-wp-fetchpriority="auto"></script>
<script type="text/javascript" src="/x.js" id="x-js" fetchpriority="high"></script>Script modules: <link rel="modulepreload" href="/b.js" id="b-js-modulepreload">
<link rel="modulepreload" href="/c.js" id="c-js-modulepreload">
<link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="high">
<link rel="modulepreload" href="/e.js" id="e-js-modulepreload">
<link rel="modulepreload" href="/z.js" id="z-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="auto">
<link rel="modulepreload" href="/y.js" id="y-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="auto">
<script type="module" src="/a.js" id="a-js-module" fetchpriority="low"></script>
<script type="module" src="/x.js" id="x-js-module" fetchpriority="high"></script> |
That's good, I misinterpreted the changes. Thanks for the additional test and clarification, they're helpful 👍 I'll do my best to give this thorough review early next week. Do you have thoughts on this point?
Would it make sense to always derive a module's fetch priority from whatever enqueues it, so that it inherits the highest fetch priority of an enqueued module? We'd start with a graph like this ---
config:
theme: base
---
flowchart LR
classDef HI stroke-width:1px, stroke:#ff0000, fill:#ff000022
classDef LO stroke-width:1px, stroke:#0000ff, fill:#0000ff22
classDef ENQUEUE stroke-width:4px
X["X::low"]
X:::LO
X:::ENQUEUE
Y["Y::auto"]
Z["Z::high"]
Z:::HI
X:::ENQUEUE
Y:::ENQUEUE
Z:::ENQUEUE
X ---> A
Y ---> B
Z ---> C
A --> D & E
B --> E
C --> E & F
And the result would be this ---
config:
theme: base
---
flowchart LR
classDef HI stroke-width:1px, stroke:#ff0000, fill:#ff000022
classDef LO stroke-width:1px, stroke:#0000ff, fill:#0000ff22
classDef ENQUEUE stroke-width:4px
X["X::low"]
X:::LO
X:::ENQUEUE
Y["Y::auto"]
Z["Z::high"]
Z:::HI
X:::ENQUEUE
Y:::ENQUEUE
Z:::ENQUEUE
X ---> A
Y ---> B
Z ---> C
A --> D & E
B --> E
C --> E & F
A["A::low"]:::LO
C["C::high"]:::HI
D["D::low"]:::LO
E["E::high"]:::HI
F["F::high"]:::HI
|
Co-authored-by: Jon Surrell <[email protected]>
|
@sirreal That's a good question, and I'm not entirely sure which way is better, or if “enqueued-script Note this changes the priorities from #9770 (comment) which were originally: ---
config:
theme: base
---
flowchart LR
classDef HI stroke-width:1px, stroke:#ff0000, fill:#ff000022
classDef LO stroke-width:1px, stroke:#0000ff, fill:#0000ff22
classDef ENQUEUE stroke-width:4px
A:::LO
Y:::HI
Z:::HI
A["A::low"]
B["B"]
C["C"]
E["E"]
D["D::high"]
D:::HI
X["X::high"]
Y["Y"]
Z["Z"]
X:::HI
A --> B --> C --> E & D
A:::ENQUEUE
X --> D & Y --> Z
X:::ENQUEUE
But now they are: ---
config:
theme: base
---
flowchart LR
classDef HI stroke-width:1px, stroke:#ff0000, fill:#ff000022
classDef LO stroke-width:1px, stroke:#0000ff, fill:#0000ff22
classDef ENQUEUE stroke-width:4px
A:::LO
B:::LO
C:::LO
E:::LO
Y:::HI
Z:::HI
A["A::low"]
B["B"]
C["C"]
E["E"]
D["D::high"]
D:::HI
X["X::high"]
Y["Y"]
Z["Z"]
X:::HI
A --> B --> C --> E & D
A:::ENQUEUE
X --> D & Y --> Z
X:::ENQUEUE
|
|
Relatedly: If an enqueued script module has a |
I'm not sure of the specifics on modulepreload in this case, but it's a good question. I understand that they're only browser hints, they give additional information to the browser and it decides exactly what to do with them and how to prioritize them. I'd hope that browsers are capable of prioritizing those preloads well so that there low priority module preloads are not harmful to things like LCP. It's worth doing some testing, but let's leave that for another PR if we want to make any changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tested this and it seems to work well. I think this does the correct thing of inheriting fetch priority from an enqueued script.
I left a few minor comments.
| if ( isset( $this->dependents_map[ $id ] ) ) { | ||
| return $this->dependents_map[ $id ]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no invalidation in this caching. That's OK right now because we only start exploring the graph when it's time to start outputting tags. The method is private, and it's likely fine, but there is an implicit timing constraint here where this can contain inaccurate information if scripts are added later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I just wrote in #9867 (comment), I think we should explicitly warn and noop when attempting to register/enqueue a script module after the importmap has been printed. This would avoid the issue with cache invalidation, and it would address problems that can arise if a script module with dependencies is enqueued without the dependencies being in the already-printed importmap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's critical to figure.
Once the importmap is printed, there's nothing to be done, so the restriction seems reasonable. If this were in an output buffer (something I know you've been working on), there could be a possibility of updating the importmap later before flushing.
There's starting to be support for multiple importmaps, but FireFox does not support it. In the future we may be able to print an early and a late importmap.
src/wp-includes/class-wp-scripts.php
Outdated
| static $priority_mapping = array( | ||
| 'low' => 0, | ||
| 'auto' => 1, | ||
| 'high' => 2, | ||
| ); | ||
|
|
||
| $highest_priority_index = $priority_mapping[ $fetchpriority ]; | ||
|
|
||
| foreach ( $this->get_dependents( $handle ) as $dependent_handle ) { | ||
| $dependent_priority = $this->get_highest_fetchpriority_with_dependents( $dependent_handle, $checked ); | ||
| if ( is_string( $dependent_priority ) ) { | ||
| $highest_priority_index = max( | ||
| $highest_priority_index, | ||
| $priority_mapping[ $dependent_priority ] | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| $highest_priority = array_search( $highest_priority_index, $priority_mapping, true ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same functionality but implemented differently from in modules:
Notably, there the array_search happens in the loop to get a numeric value for fetchpriority comparison.
I'd like implementations to match.
There's also an opportunity to exit the loop as soon once a high priority is found (I commented similarly on the modules implementation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about 334a82b?
Replying to myself: No, I was not interpreting the changes correctly. I had mixed up dependents with dependencies in the graph exploration so had backwards expectations. The implementation does the right thing. |
…into add/dependent-fetchpriority-harmony
Co-authored-by: Jon Surrell <[email protected]>
…into add/dependent-fetchpriority-harmony
…into add/dependent-fetchpriority-harmony
51f6c5d to
eada641
Compare
…ies. This introduces a "fetchpriority bumping" mechanism for both classic scripts (`WP_Scripts`) and script modules (`WP_Script_Modules`). When a script with a higher `fetchpriority` is enqueued, any of its dependencies will have their `fetchpriority` elevated to match that of the highest-priority dependent. This ensures that all assets in a critical dependency chain are loaded with the appropriate priority, preventing a high-priority script from being blocked by a low-priority dependency. This is similar to logic used in script loading strategies to ensure that a blocking dependent causes delayed (`async`/`defer`) dependencies to also become blocking. See #12009. When a script's `fetchpriority` is escalated, its original, registered priority is added to the tag via a `data-wp-fetchpriority` attribute. This matches the addition of the `data-wp-strategy` parameter added when the resulting loading strategy does not match the original. Developed in #9770. Follow-up to [60704]. Props westonruter, jonsurrell. Fixes #61734. git-svn-id: https://develop.svn.wordpress.org/trunk@60931 602fd350-edb4-49c9-b593-d223f7449a82
…ies. This introduces a "fetchpriority bumping" mechanism for both classic scripts (`WP_Scripts`) and script modules (`WP_Script_Modules`). When a script with a higher `fetchpriority` is enqueued, any of its dependencies will have their `fetchpriority` elevated to match that of the highest-priority dependent. This ensures that all assets in a critical dependency chain are loaded with the appropriate priority, preventing a high-priority script from being blocked by a low-priority dependency. This is similar to logic used in script loading strategies to ensure that a blocking dependent causes delayed (`async`/`defer`) dependencies to also become blocking. See #12009. When a script's `fetchpriority` is escalated, its original, registered priority is added to the tag via a `data-wp-fetchpriority` attribute. This matches the addition of the `data-wp-strategy` parameter added when the resulting loading strategy does not match the original. Developed in WordPress/wordpress-develop#9770. Follow-up to [60704]. Props westonruter, jonsurrell. Fixes #61734. Built from https://develop.svn.wordpress.org/trunk@60931 git-svn-id: http://core.svn.wordpress.org/trunk@60267 1a063a9b-81f0-0310-95a4-ce76da25c4cd
…ies. This introduces a "fetchpriority bumping" mechanism for both classic scripts (`WP_Scripts`) and script modules (`WP_Script_Modules`). When a script with a higher `fetchpriority` is enqueued, any of its dependencies will have their `fetchpriority` elevated to match that of the highest-priority dependent. This ensures that all assets in a critical dependency chain are loaded with the appropriate priority, preventing a high-priority script from being blocked by a low-priority dependency. This is similar to logic used in script loading strategies to ensure that a blocking dependent causes delayed (`async`/`defer`) dependencies to also become blocking. See #12009. When a script's `fetchpriority` is escalated, its original, registered priority is added to the tag via a `data-wp-fetchpriority` attribute. This matches the addition of the `data-wp-strategy` parameter added when the resulting loading strategy does not match the original. Developed in WordPress/wordpress-develop#9770. Follow-up to [60704]. Props westonruter, jonsurrell. Fixes #61734. Built from https://develop.svn.wordpress.org/trunk@60931 git-svn-id: https://core.svn.wordpress.org/trunk@60267 1a063a9b-81f0-0310-95a4-ce76da25c4cd
|
There is an issue here: https://github.com/WordPress/wordpress-develop/blame/81b3c68c692b79b9bbaed0f8e16dfa51147b356b/src/wp-includes/class-wp-scripts.php#L1073. The checked param of the function needs a reference the way it works now you only save the checked one way down the recursive tree not in parallel. This can cause huge performance problems. Propsed a fix to not have sometimes page loads of 10-20s #10459 |
This is based on feedback from @sirreal in WordPress/gutenberg#70173 (review):
This PR ensures that if you have a dependent script module with
fetchpriority=high, that any recursive dependencies will also get printed withfetchpriority=highregardless of the originally-registeredfetchpriority, in which case the originallyfetchpriorityis emitted as adata-wp-fetchpriorityattribute on theSCRIPT[type=module]orLINK[rel=modulepreload]for the sake of debugging. This is highly analogous to how non-module scripts check their dependents to see if any haveasyncordefer, where if a blocking script depends on adeferscript, thedeferscript will become blocking and adata-wp-strategyattribute is added to theSCRIPTtag.The same is also implemented here for non-module scripts.
Also, this PR addresses this feedback in WordPress/gutenberg#70173 (comment):
The script
@wordpress/a11yis set to have afetchpriority=low.Prompt for Gemini CLI:
Output:
Trac ticket: https://core.trac.wordpress.org/ticket/61734
TODO
comment-replyand interactive blocks view modules are getting the expectedfetchpriority.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.