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>
214 lines
12 KiB
Markdown
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)
|