The interactive page-load path fires DOMContentLoaded before any <script defer> has executed. The HTML spec ("the end") requires the opposite order: parser finishes, defer scripts execute in document order, then DOMContentLoaded, then (after subresources settle) load. This is the second of two bugs behind x.com's "Something went wrong" fallback page.
What happens
On the interactive (progressive first paint) path:
src/Starling.Engine/Engine.cs:737 calls RunCriticalScripts(session, deferAsync: true, includeParserDeferred: false, ct).
- Inside,
RunOrderedScripts returns early before the defer batch when includeDefer is false (Engine.cs:1431-1432), and module scripts are skipped (Engine.cs:1343-1344).
s.Session.FireDomContentLoaded() still runs unconditionally (Engine.cs:1353). So DOMContentLoaded fires with zero defer scripts executed.
- The defer bundles are only fetched after first paint (
FetchDeferredAsync, Engine.cs:769-772) and only execute in RunDeferredScriptsAsync (Engine.cs:780 → 1374/1381).
The headless path is spec-correct: Engine.cs:1213 passes includeParserDeferred: true, so defer, async, and module scripts all run before DOMContentLoaded. The load event ordering is also already correct (FireLoad at Engine.cs:1392 runs after the bundles plus a quiescence pump).
Sites that gate boot checks on DOMContentLoaded conclude their scripts failed to load. x.com's watchdog checks window.__SCRIPTS_LOADED__['main'], which is set at the end of its defer bundle main.565e4e9a.js — so the check always fails and the error panel shows.
Verified 2026-06-10 with a live engine repro: DOMContentLoaded logged before any defer script ran, and the fallback panel was display:block at first paint.
Fix
Real fix: on the interactive path, do not fire DOMContentLoaded in RunCriticalScripts. Fire it in RunDeferredScriptsAsync, after RunDeferredClassicScripts and RunModuleScripts and before the async-script step (async scripts carry no order guarantee relative to DOMContentLoaded). First paint before DOMContentLoaded is fine and matches real browsers — they paint progressively during parse. Also start fetching defer scripts during parse instead of after first paint (the spec fetches in parallel and executes at the end), so DOMContentLoaded is not delayed by network on top of execution time.
Why the current shape exists (the band-aid pressure): the early DOMContentLoaded lets "synchronous handlers see the parsed DOM" before a fast first paint. That pressure comes from slow bundle execution in the Starling JS engine — x.com's bundles take about two minutes in the deferred pump (deferred.summary: pump2=110668ms total=147706ms from the live session). Spec order is the real fix. First paint does not need DOMContentLoaded to have fired.
Context
Found during the x.com/nasa boot investigation on 2026-06-10 (branch feat/js-stack-trampoline). Interlocks with two sibling issues filed from the same investigation: "document.readyState is always complete" (either bug alone shows x.com's fallback) and "ScriptFetcher turns large defer scripts into async scripts" (which would keep the bundles after DOMContentLoaded even once this fires in the right place).
The interactive page-load path fires
DOMContentLoadedbefore any<script defer>has executed. The HTML spec ("the end") requires the opposite order: parser finishes, defer scripts execute in document order, thenDOMContentLoaded, then (after subresources settle)load. This is the second of two bugs behind x.com's "Something went wrong" fallback page.What happens
On the interactive (progressive first paint) path:
src/Starling.Engine/Engine.cs:737callsRunCriticalScripts(session, deferAsync: true, includeParserDeferred: false, ct).RunOrderedScriptsreturns early before the defer batch whenincludeDeferis false (Engine.cs:1431-1432), and module scripts are skipped (Engine.cs:1343-1344).s.Session.FireDomContentLoaded()still runs unconditionally (Engine.cs:1353). SoDOMContentLoadedfires with zero defer scripts executed.FetchDeferredAsync,Engine.cs:769-772) and only execute inRunDeferredScriptsAsync(Engine.cs:780→1374/1381).The headless path is spec-correct:
Engine.cs:1213passesincludeParserDeferred: true, so defer, async, and module scripts all run beforeDOMContentLoaded. Theloadevent ordering is also already correct (FireLoadatEngine.cs:1392runs after the bundles plus a quiescence pump).Sites that gate boot checks on
DOMContentLoadedconclude their scripts failed to load. x.com's watchdog checkswindow.__SCRIPTS_LOADED__['main'], which is set at the end of its defer bundlemain.565e4e9a.js— so the check always fails and the error panel shows.Verified 2026-06-10 with a live engine repro:
DOMContentLoadedlogged before any defer script ran, and the fallback panel wasdisplay:blockat first paint.Fix
Real fix: on the interactive path, do not fire
DOMContentLoadedinRunCriticalScripts. Fire it inRunDeferredScriptsAsync, afterRunDeferredClassicScriptsandRunModuleScriptsand before the async-script step (async scripts carry no order guarantee relative toDOMContentLoaded). First paint beforeDOMContentLoadedis fine and matches real browsers — they paint progressively during parse. Also start fetching defer scripts during parse instead of after first paint (the spec fetches in parallel and executes at the end), soDOMContentLoadedis not delayed by network on top of execution time.Why the current shape exists (the band-aid pressure): the early
DOMContentLoadedlets "synchronous handlers see the parsed DOM" before a fast first paint. That pressure comes from slow bundle execution in the Starling JS engine — x.com's bundles take about two minutes in the deferred pump (deferred.summary: pump2=110668ms total=147706msfrom the live session). Spec order is the real fix. First paint does not needDOMContentLoadedto have fired.Context
Found during the x.com/nasa boot investigation on 2026-06-10 (branch
feat/js-stack-trampoline). Interlocks with two sibling issues filed from the same investigation: "document.readyState is always complete" (either bug alone shows x.com's fallback) and "ScriptFetcher turns large defer scripts into async scripts" (which would keep the bundles afterDOMContentLoadedeven once this fires in the right place).