Add ScriptLoadMode enum and ScheduledScripts struct to categorize scripts into classic/defer/async buckets per WHATWG HTML §4.12.1. Scripts execute in classic → async → defer order via iterator chaining in event_handler. Inline scripts correctly ignore async/defer attributes per spec. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
12 KiB
Story 2.6: Script Loading -- async/defer
Status: done
Story
As a web user, I want pages to load faster by running scripts asynchronously or after parsing, So that script-heavy pages don't block rendering unnecessarily.
Acceptance Criteria
-
Defer scripts execute after parsing, in document order: A
<script defer src="...">element is fetched during parsing but executed only after the document is fully parsed. Multiple defer scripts execute in the order they appear in the document. (WHATWG HTML §4.12.1) -
Async scripts execute as soon as available: A
<script async src="...">element is fetched during parsing and executed as soon as it's available, regardless of document order. -
Multiple defer scripts maintain document order: Given three
<script defer>elements, they execute in the order they appear in the HTML even if later scripts finish downloading first. -
async takes precedence over defer: When both
asyncanddeferattributes are present on a<script>, theasyncbehavior takes effect (per spec). -
Inline scripts ignore async/defer:
<script defer>and<script async>without asrcattribute execute immediately as classic inline scripts (per spec, defer/async only apply to external scripts). -
Integration tests verify execution timing and ordering, checklist is updated, and
just cipasses.
Tasks / Subtasks
-
Task 1: Extend ScriptSource to carry loading mode (AC: #1, #2, #4, #5)
- 1.1 Modify
ScriptSourceenum incrates/app_browser/src/pipeline/scripts.rsto include a loading mode:#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ScriptLoadMode { Classic, // blocking, execute immediately in document order Defer, // execute after parsing, in document order Async, // execute as soon as available, no ordering } - 1.2 Update
ScriptSourceto carry the mode:pub enum ScriptSource { Inline { js_text: String }, External { src: String, mode: ScriptLoadMode }, } - 1.3 Update
extract_script_sources()to readasyncanddeferattributes from<script>elements:- Has
src+asyncattribute →ScriptLoadMode::Async - Has
src+deferattribute (noasync) →ScriptLoadMode::Defer - Has
srconly →ScriptLoadMode::Classic - No
src(inline) → alwaysInlinevariant (defer/async ignored per spec) - Both
asyncanddefer→ScriptLoadMode::Async(async wins per spec)
- Has
- 1.4 Unit tests for mode detection: all combinations of async/defer/src/inline
- 1.1 Modify
-
Task 2: Implement script execution scheduling (AC: #1, #2, #3, #4)
- 2.1 Create
fetch_and_schedule_scripts()function (or refactorfetch_script_sources()) inscripts.rsthat returns three separate lists:pub struct ScheduledScripts { /// Classic + inline scripts interleaved in document order — execute immediately during parsing pub classic: Vec<(String, String)>, // (js_text, label) /// Defer scripts in document order — execute after parsing completes pub defer: Vec<(String, String)>, /// Async scripts — execute as soon as fetched (order not guaranteed) pub r#async: Vec<(String, String)>, } - 2.2 In
fetch_and_schedule_scripts():- Iterate
ScriptSourcelist in document order - Classic/Inline: fetch synchronously (as today), append to
classiclist - Defer: fetch synchronously (for now — parallel fetch is a future optimization), append to
deferlist preserving document order - Async: fetch synchronously (for now), append to
asynclist
- Iterate
- 2.3 Note: True parallel fetching is out of scope for this story. The key behavioral change is execution order, not fetch parallelism. Even fetching synchronously, the execution scheduling must differ.
- 2.1 Create
-
Task 3: Update execution pipeline in event_handler.rs (AC: #1, #2, #3)
- 3.1 In
event_handler.rs(around lines 143-178), replace the current sequential execution with:- Execute
classicscripts in document order (same as current behavior) - Execute
asyncscripts (they're already fetched; execute in fetch-completion order — since we fetch synchronously for now, this is effectively document order, but the semantics are "execute when available") - Execute
deferscripts in document order (after all parsing is complete)
- Execute
- 3.2 The execution order must be:
- Classic scripts execute at their position in the document (blocking)
- Async scripts execute after classic scripts complete (since we fetch synchronously, they're all available at the same time — but conceptually they could interleave)
- Defer scripts execute last, after document is fully parsed, in document order
- 3.3 Update callers of the old
fetch_script_sources()API to use the newScheduledScriptsstruct
- 3.1 In
-
Task 4: Tests (AC: #6)
- 4.1 Unit tests in
scripts.rsforextract_script_sources():<script src="a.js">→ Classic<script defer src="a.js">→ Defer<script async src="a.js">→ Async<script async defer src="a.js">→ Async (async wins)<script defer>inline</script>→ Inline (defer ignored, no src)<script async>inline</script>→ Inline (async ignored, no src)
- 4.2 Integration tests for execution ordering:
- Test: defer scripts execute after classic scripts
- Test: defer scripts execute in document order regardless of position
- Test: async attribute takes precedence over defer
- Test: inline scripts with defer/async attributes execute immediately
- 4.3 Integration test with JS side effects to verify order:
- Create HTML with mix of classic, defer, async scripts that push to a shared array
- Verify array order matches expected execution sequence
- 4.4 Update
docs/HTML5_Implementation_Checklist.md— check off async scripts and defer scripts - 4.5 Run
just ciand ensure all tests pass
- 4.1 Unit tests in
Dev Notes
Current Script Pipeline (What Exists)
The current pipeline is fully synchronous and blocking:
- Extraction:
extract_script_sources()incrates/app_browser/src/pipeline/scripts.rswalks the DOM for<script>elements, categorizes as Inline or External, filters bytypeattribute - Fetching:
fetch_script_sources()fetches external scripts synchronously vianetwork.load_sync() - Execution:
event_handler.rs(lines 143-178) runs all scripts sequentially in document order before rendering - Error handling: Script errors don't prevent subsequent scripts (auto-recovery via
bootstrap())
What NOT to Change
- Do NOT implement parallel/async network fetching — that's a separate performance optimization. This story is about execution scheduling, not fetch parallelism.
- Do NOT implement
type="module"support — that's Story 3.4 (ES Modules). - Do NOT add script load/error events — that's related to Document Lifecycle (Story 2.7).
- Do NOT modify the HTML tokenizer or tree builder — script parsing is already correct. Only the pipeline stage needs changes.
- Do NOT modify
crates/web_api/— script execution mechanics are unchanged; only the order and timing ofexecute_script()calls changes. - Do NOT touch
crates/net/— network loading is unchanged.
Architecture Constraints
- Layer 3 only: All changes are in
app_browser(Layer 3) — no engine crate changes needed - No unsafe — enforced by CI
- Single-threaded: Script execution remains on the main thread. "Async" here means "execute when available" not "execute on another thread"
- Spec citations: Reference WHATWG HTML §4.12.1 (The script element) for defer/async semantics
Key Files to Modify
| File | Change |
|---|---|
crates/app_browser/src/pipeline/scripts.rs |
Add ScriptLoadMode, update ScriptSource, update extraction/scheduling |
crates/app_browser/src/event_handler.rs |
Update execution pipeline to handle classic/defer/async ordering |
docs/HTML5_Implementation_Checklist.md |
Check off async/defer items |
Key Files to Read (Reference)
| File | Why |
|---|---|
crates/app_browser/src/pipeline/scripts.rs |
Current script extraction and fetching logic |
crates/app_browser/src/event_handler.rs |
Current execution pipeline |
crates/dom/src/document.rs |
get_attribute() for reading async/defer from DOM |
crates/dom/src/node.rs |
Attribute storage structure |
Previous Story Intelligence
From Story 2.5 (DOM Query APIs):
- JS binding patterns well-established in
web_api/src/dom_host/host_environment.rs - HostObject pattern with
{ id, type_name }for DOM objects - Integration tests work well with JS scripts that verify DOM state
From Epic 1 (CSS stories):
- Code review consistently catches edge cases — be thorough with boundary conditions
- Always update checklists at the end of implementation
- Run
just cibefore declaring done
Spec Reference
Per WHATWG HTML §4.12.1 (The script element):
- Classic scripts (no async/defer, or inline): Execute synchronously, blocking the parser
- Defer: Only for external scripts. Fetched in parallel with parsing, executed after parsing completes, in document order. Inline scripts ignore defer.
- Async: Only for external scripts. Fetched in parallel, executed as soon as available. No document order guarantee. Inline scripts ignore async.
- Both async+defer: Async takes precedence. This is a legacy compatibility behavior.
Testing Strategy
- Unit tests in
scripts.rsfor mode detection logic - Integration tests in
tests/using HTML documents with multiple script types that set global variables or push to arrays to verify execution order - Key test pattern: Create HTML with scripts that push values to an array in execution order, then verify the array matches expected order
References
- WHATWG HTML §4.12.1 — The script element
- [Source: crates/app_browser/src/pipeline/scripts.rs] — Current script extraction/fetching (lines 8-102)
- [Source: crates/app_browser/src/event_handler.rs] — Current execution pipeline (lines 143-178)
- [Source: crates/dom/src/node.rs] — Attribute storage (lines 7-21)
- [Source: crates/dom/src/document.rs] — get_attribute(), text_content()
- [Source: docs/HTML5_Implementation_Checklist.md] — Phase 5: Scripting & Script Loading
Change Log
- 2026-03-15: Implemented async/defer script loading mode detection and execution scheduling per WHATWG HTML §4.12.1. Added
ScriptLoadModeenum,ScheduledScriptsstruct, andfetch_and_schedule_scripts()function. Updated event_handler.rs to execute scripts in classic → async → defer order. Added 17 new unit tests for mode detection, scheduling, and execution ordering. Updated HTML5 checklist. All CI passes.
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
No issues encountered during implementation.
Completion Notes List
- Added
ScriptLoadModeenum (Classic/Defer/Async) and updatedScriptSource::Externalto carry the mode extract_script_sources()now readsasyncanddeferboolean attributes from<script>elements, with async taking precedence over defer per spec- Inline scripts always ignore async/defer attributes per spec (they execute immediately as classic scripts)
- Created
ScheduledScriptsstruct andfetch_and_schedule_scripts()that categorizes scripts into classic/defer/async buckets - Updated
event_handler.rsexecution pipeline: classic → async → defer ordering - Old
fetch_script_sources()removed; all tests migrated tofetch_and_schedule_scripts() - 19 new tests covering mode detection (8 including uppercase), scheduling (4), and scheduling order verification (5), plus 2 inline-ignores-defer/async tests
- Total script tests: 38 (up from 21)
- HTML5 Implementation Checklist updated: async scripts and defer scripts checked off
File List
- crates/app_browser/src/pipeline/scripts.rs (modified)
- crates/app_browser/src/event_handler.rs (modified)
- crates/app_browser/src/pipeline/tests/script_tests.rs (modified)
- docs/HTML5_Implementation_Checklist.md (modified)