Skip to content

ScriptFetcher turns large defer scripts into async scripts — breaks script-order guarantees (band-aid for slow JS execution) #72

Description

@codymullins

External scripts over 200KB that the parser inserted with defer (or with no attribute) are silently reclassified as async. Defer scripts must run in document order before DOMContentLoaded. Async scripts run whenever. This reclassification is itself a band-aid for slow JS execution, and it breaks the ordering contract sites rely on.

What happens

src/Starling.Engine/ScriptFetcher.cs:233-239 promotes large None/Defer external scripts to Async (the DeferringLargeScript log at line 237 records it, but the page cannot see it). All three x.com bundles qualify: vendor.b6dfc27a.js (689KB), i18n/en.ee2bc0fa.js (665KB), main.565e4e9a.js (1.4MB).

Document order is preserved today only by accident — RunAsyncScripts iterates Fetcher.Scripts in document order (src/Starling.Engine/Engine.cs:1456-1463). Nothing guarantees that stays true.

The bigger problem: even after the DOMContentLoaded-ordering bug (sibling issue) is fixed, this promotion would still push the bundles into the async step, after DOMContentLoaded — keeping x.com's "Something went wrong" watchdog firing.

Fix

This mechanism is a band-aid. It exists so huge bundles do not block the render-critical script phase, because executing multi-megabyte bundles is slow in the Starling JS engine (minutes for x.com). The real root cause is JS execution speed, which is tracked separately (inline caches and hidden-class work exist on an unmerged branch).

Real fix: remove the promotion for parser-inserted defer and ordered scripts and restore spec classification. Keep first paint fast the spec-legal way instead: paint first, then run the defer batch, then fire DOMContentLoaded (the sibling DOMContentLoaded issue describes that pipeline). DOMContentLoaded then arrives later than Chromium's by the execution time. That is honest, order-correct degradation under a slow engine — not a lie about what kind of script the page wrote.

Acceptable narrow band-aid: keep a size heuristic only for scripts that are already async-eligible, or behind a flag for benchmarking. Never for defer or ordered scripts.

Context

Found during the x.com/nasa boot investigation on 2026-06-10 (branch feat/js-stack-trampoline). Sibling issues: "Interactive page load fires DOMContentLoaded before <script defer> runs" and "document.readyState is always complete".

Metadata

Metadata

Assignees

No one assigned

    Labels

    browserRelates to the browser subsystembugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions