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

214 lines
12 KiB
Markdown

# Story 2.6: Script Loading -- async/defer
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## 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
- [x] Task 1: Extend ScriptSource to carry loading mode (AC: #1, #2, #4, #5)
- [x] 1.1 Modify `ScriptSource` enum in `crates/app_browser/src/pipeline/scripts.rs` to include a loading mode:
```rust
#[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
}
```
- [x] 1.2 Update `ScriptSource` to carry the mode:
```rust
pub enum ScriptSource {
Inline { js_text: String },
External { src: String, mode: ScriptLoadMode },
}
```
- [x] 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 `defer` → `ScriptLoadMode::Async` (async wins per spec)
- [x] 1.4 Unit tests for mode detection: all combinations of async/defer/src/inline
- [x] Task 2: Implement script execution scheduling (AC: #1, #2, #3, #4)
- [x] 2.1 Create `fetch_and_schedule_scripts()` function (or refactor `fetch_script_sources()`) in `scripts.rs` that returns three separate lists:
```rust
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)>,
}
```
- [x] 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
- [x] 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.
- [x] Task 3: Update execution pipeline in event_handler.rs (AC: #1, #2, #3)
- [x] 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)
- [x] 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
- [x] 3.3 Update callers of the old `fetch_script_sources()` API to use the new `ScheduledScripts` struct
- [x] Task 4: Tests (AC: #6)
- [x] 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)
- [x] 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
- [x] 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
- [x] 4.4 Update `docs/HTML5_Implementation_Checklist.md` — check off async scripts and defer scripts
- [x] 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](https://html.spec.whatwg.org/multipage/scripting.html#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)