Files
rust_browser/_bmad-output/implementation-artifacts/2-6-script-loading-async-defer.md
Zachary D. Rowitsch fc1352a081 Implement async/defer script loading with code review fixes (§4.12.1)
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>
2026-03-15 00:15:44 -04:00

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

  1. 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)

  2. 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.

  3. 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.

  4. async takes precedence over defer: When both async and defer attributes are present on a <script>, the async behavior takes effect (per spec).

  5. Inline scripts ignore async/defer: <script defer> and <script async> without a src attribute execute immediately as classic inline scripts (per spec, defer/async only apply to external scripts).

  6. Integration tests verify execution timing and ordering, checklist is updated, and just ci passes.

Tasks / Subtasks

  • Task 1: Extend ScriptSource to carry loading mode (AC: #1, #2, #4, #5)

    • 1.1 Modify ScriptSource enum in crates/app_browser/src/pipeline/scripts.rs to 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 ScriptSource to carry the mode:
      pub enum ScriptSource {
          Inline { js_text: String },
          External { src: String, mode: ScriptLoadMode },
      }
      
    • 1.3 Update extract_script_sources() to read async and defer attributes from <script> elements:
      • Has src + async attribute → ScriptLoadMode::Async
      • Has src + defer attribute (no async) → ScriptLoadMode::Defer
      • Has src only → ScriptLoadMode::Classic
      • No src (inline) → always Inline variant (defer/async ignored per spec)
      • Both async and deferScriptLoadMode::Async (async wins per spec)
    • 1.4 Unit tests for mode detection: all combinations of async/defer/src/inline
  • Task 2: Implement script execution scheduling (AC: #1, #2, #3, #4)

    • 2.1 Create fetch_and_schedule_scripts() function (or refactor fetch_script_sources()) in scripts.rs that 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 ScriptSource list in document order
      • Classic/Inline: fetch synchronously (as today), append to classic list
      • Defer: fetch synchronously (for now — parallel fetch is a future optimization), append to defer list preserving document order
      • Async: fetch synchronously (for now), append to async list
    • 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.
  • 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:
      1. Execute classic scripts in document order (same as current behavior)
      2. Execute async scripts (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")
      3. Execute defer scripts in document order (after all parsing is complete)
    • 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 new ScheduledScripts struct
  • Task 4: Tests (AC: #6)

    • 4.1 Unit tests in scripts.rs for extract_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 ci and ensure all tests pass

Dev Notes

Current Script Pipeline (What Exists)

The current pipeline is fully synchronous and blocking:

  1. Extraction: extract_script_sources() in crates/app_browser/src/pipeline/scripts.rs walks the DOM for <script> elements, categorizes as Inline or External, filters by type attribute
  2. Fetching: fetch_script_sources() fetches external scripts synchronously via network.load_sync()
  3. Execution: event_handler.rs (lines 143-178) runs all scripts sequentially in document order before rendering
  4. 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 of execute_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 ci before 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.rs for 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 ScriptLoadMode enum, ScheduledScripts struct, and fetch_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 ScriptLoadMode enum (Classic/Defer/Async) and updated ScriptSource::External to carry the mode
  • extract_script_sources() now reads async and defer boolean 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 ScheduledScripts struct and fetch_and_schedule_scripts() that categorizes scripts into classic/defer/async buckets
  • Updated event_handler.rs execution pipeline: classic → async → defer ordering
  • Old fetch_script_sources() removed; all tests migrated to fetch_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)