Script Loader: Defer single-page admin init until DOMContentLoaded (Trac #65103)#11611
Script Loader: Defer single-page admin init until DOMContentLoaded (Trac #65103)#11611itzmekhokan wants to merge 2 commits intoWordPress:trunkfrom
Conversation
|
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. |
The Connectors and Font Library admin screens register a `-prerequisites`
classic-script handle with an empty `src` and attach an inline script that
does `import("@wordpress/boot").then(mod => mod.init(...))`. Because the
handle has no `src`, only the inline is printed, and it runs as a classic
script the moment the parser reaches it.
On fast CDN-fronted hosts (WordPress VIP, CloudFront, etc.) in Chrome,
`@wordpress/boot` is already `<link rel="modulepreload">`-ed, so the
dynamic import resolves and the module evaluates before the parser has
reached the classic deps it relies on (`wp-private-apis`, `wp-components`,
`wp-theme`). At its top-level `@wordpress/boot` reads
`window.wp.theme.privateApis`, which is still undefined at that point,
causing `unlock(undefined)` to throw "Cannot unlock an undefined object".
The mount element stays empty and `initSinglePage` / `init` never runs.
Wrap the dynamic import in `DOMContentLoaded`. The HTML spec guarantees
`DOMContentLoaded` fires only after every parser-blocking classic
`<script>` has executed, so all required globals are populated before
`@wordpress/boot` evaluates. A `document.readyState` guard preserves the
existing behavior when the inline is evaluated after DOM ready (e.g.
injected late).
Applied to all four auto-generated files that share the pattern:
* src/wp-includes/build/pages/options-connectors/page.php
* src/wp-includes/build/pages/options-connectors/page-wp-admin.php
* src/wp-includes/build/pages/font-library/page.php
* src/wp-includes/build/pages/font-library/page-wp-admin.php
Fixes #65103.
Made-with: Cursor
Follow-up to [65103] feedback. Adjust the multi-line `/* */` comments added for the DOMContentLoaded deferral so they match the WordPress PHP inline-documentation conventions: no DocBlock-style summary-then-blank separator (that structure is reserved for `/** */`), and use the short `See #65103.` form for Trac references instead of `See Trac #65103.` to match usages throughout core (`See #38883`, `See #40146`, etc.). See #65103. Made-with: Cursor
The change shouldn't be done in core here, but rather in Gutenberg. The related code is located here: https://github.com/WordPress/gutenberg/blob/9083dd07b5705f281aff2a68f647f62b3cdf0868/packages/wp-build/templates/page-wp-admin.php.template#L156-L164 |
Trac ticket
https://core.trac.wordpress.org/ticket/65103
Summary
Fixes a race condition that prevents the Connectors (
/wp-admin/options-connectors.php) and Font Library admin screens from mounting in Chrome on fast-CDN-fronted hosts (reproducible on WordPress VIP). The failure surfaces as an empty mount div plusUncaught Error: Cannot unlock an undefined objectin the console.Root cause
The four auto-generated files below register a
-prerequisitesclassic-script handle with an emptysrcand attach an inline script that does a dynamic ESM import:Because the handle has no
src, only the inline is printed, and it runs as a classic<script>the instant the HTML parser reaches it.@wordpress/bootis<link rel="modulepreload">-ed in<head>. On a fast CDN the bundle is effectively free, so the dynamic import resolves and the module evaluates before the parser has reached the classic deps it relies on (wp-private-apis,wp-components,wp-theme).At its top-level
@wordpress/bootreadswindow.wp.theme.privateApis, which is stillundefinedat that point, sounlock(undefined)throws andinitSinglePage/initnever runs.Only Chrome + fast CDN reliably loses the race; Firefox/Safari schedule module eval slightly later, and local dev is slow enough that the classic deps usually finish first.
The fix (Option 1 from the ticket)
Wrap the dynamic import in
DOMContentLoaded. The HTML spec guaranteesDOMContentLoadedfires only after every parser-blocking classic<script>has executed, sowp.theme.privateApis(and the rest) are populated before@wordpress/bootevaluates. Adocument.readyState === "loading"guard preserves behavior when the inline is ever re-run after DOM ready (e.g. AJAX-injected contexts).Before:
After:
Files changed
All four auto-generated files that share this pattern:
src/wp-includes/build/pages/options-connectors/page.php(init)src/wp-includes/build/pages/options-connectors/page-wp-admin.php(initSinglePage)src/wp-includes/build/pages/font-library/page.php(init)src/wp-includes/build/pages/font-library/page-wp-admin.php(initSinglePage)Each file's
wp_add_inline_script(…)call is updated, plus an explanatory comment referencing Trac #65103. No registration logic, dependency lists, script module graph, or style registration is touched.Why not the other options from the ticket
wp_add_inline_script_module()API or ascript_loader_tagfilter — out of scope for a bugfix.initSinglePageintoloader.js) would change the public module contract, require a coordinated Gutenberg-side change, and migrate every downstream consumer of the pattern.Follow-up note for committers
These files are marked "Auto-generated by build process. Do not edit this file manually." The generator lives in Gutenberg's build pipeline. A search of the local Gutenberg checkout finds no matching template string, so the
wordpress-developcopies are effectively the current source of truth — but the Gutenberg-side template should receive the same change (or the next sync from Gutenberg will silently revert this). Happy to open a follow-up Gutenberg PR once this lands.Test plan
/wp-admin/options-connectors.phpand the Font Library admin screen. Confirm the app mounts and "Cannot unlock an undefined object" is not logged.document.readyStateguard: visit the page, then in the console re-evaluate the inline body and verifymod.init/mod.initSinglePageis still called exactly once (behavior preserved for late-arriving scripts).Related
wp_register_script('-prerequisites', '', …)+ inlineimport("@wordpress/boot")pattern has the same bug.