diff --git a/Packages/Infrastructure/Sources/Public/Protocols/Page.swift b/Packages/Infrastructure/Sources/Public/Protocols/Page.swift
index e925ac3..5f71622 100644
--- a/Packages/Infrastructure/Sources/Public/Protocols/Page.swift
+++ b/Packages/Infrastructure/Sources/Public/Protocols/Page.swift
@@ -5,7 +5,7 @@ import Foundation
///
/// A conforming page supplies its locale, its title, the stylesheets and scripts it needs, its head metadata, and its content; the protocol assembles the
/// rest of the document around them: the viewport declaration, the summary, canonical, and social card tags, the structured data script, the analytics
-/// tracker script, and the metadata followed by the stylesheet links in the head, and the content followed by the script tags in the body.
+/// tracker script, and the metadata followed by the stylesheet links and the deferred script tags in the head, and the content as the body.
public protocol Page: HTMLDocument, Sendable {
// MARK: Associated types
@@ -27,7 +27,7 @@ public protocol Page: HTMLDocument, Sendable {
/// The canonical URL the page is served at, rendered as a `link rel="canonical"` tag in the document head, or `nil` (the default) to omit the tag.
var canonicalURL: String? { get }
- /// The page's markup, rendered before the ``scripts``.
+ /// The page's markup, rendered as the document body.
@HTMLBuilder
var content: Content { get }
@@ -38,7 +38,10 @@ public protocol Page: HTMLDocument, Sendable {
@HTMLBuilder
var metadata: Metadata { get }
- /// The scripts loaded at the end of the document body, in order.
+ /// The scripts loaded from the document head, in order.
+ ///
+ /// Rendered as `defer`red tags: the downloads start while the head is parsed, and the scripts still execute in order only after the
+ /// document is fully parsed — the same semantics end-of-body tags would give, minus the late download start.
var scripts: [any Asset] { get }
/// The card controlling the page's link previews, rendered as Open Graph and Twitter meta tags in the document head, or `nil` (the default)
@@ -67,21 +70,15 @@ public extension Page {
nil
}
- /// The page ``content`` followed by its ``scripts``.
+ /// The page ``content``; the ``scripts`` load deferred from the ``head``.
@HTMLBuilder
var body: some HTML {
content
-
- for file in scripts {
- script(.src(file.urlPath(
- for: .js,
- version: assetVersion
- ))) {}
- }
}
/// The viewport declaration, the ``summary``, ``canonicalURL``, and ``socialCard`` tags, the ``structuredData`` script and the
- /// ``analytics`` tracker script (when provided), and the ``metadata`` followed by the ``stylesheets`` links, placed in the document head.
+ /// ``analytics`` tracker script (when provided), and the ``metadata`` followed by the ``stylesheets`` links and the deferred
+ /// ``scripts`` tags, placed in the document head.
///
/// The charset declaration is omitted: Elementary's `HTMLDocument` scaffolding already emits `` before this markup,
/// and HTML5 allows only one.
@@ -155,6 +152,16 @@ public extension Page {
))
)
}
+
+ for file in scripts {
+ script(
+ .defer,
+ .src(file.urlPath(
+ for: .js,
+ version: assetVersion
+ ))
+ ) {}
+ }
}
/// The analytics tracker is omitted unless the page provides one.
diff --git a/Packages/Infrastructure/Tests/Cases/Public/Protocols/PageTests.swift b/Packages/Infrastructure/Tests/Cases/Public/Protocols/PageTests.swift
index 2540ec2..38787fa 100644
--- a/Packages/Infrastructure/Tests/Cases/Public/Protocols/PageTests.swift
+++ b/Packages/Infrastructure/Tests/Cases/Public/Protocols/PageTests.swift
@@ -21,7 +21,7 @@ struct PageTests {
#expect(html.contains(#"name="viewport""#))
#expect(html.contains(#""#))
#expect(html.contains(#""#))
- #expect(html.contains(#""#))
+ #expect(html.contains(#""#))
#expect(html.contains("Stub content"))
}
@@ -38,13 +38,17 @@ struct PageTests {
}
@Test
- func `renders the scripts after the content`() throws {
+ func `renders the scripts deferred in the head, after the stylesheets`() throws {
let html = StubPage().render()
- let content = try #require(html.range(of: "Stub content"))
+ let stylesheet = try #require(html.range(of: "/css/stub.css"))
let script = try #require(html.range(of: "/js/stub.js"))
+ let content = try #require(html.range(of: "Stub content"))
- #expect(content.lowerBound < script.lowerBound)
+ // Deferred head scripts start downloading during head parsing but still execute, in order, only after the
+ // document is parsed — the semantics end-of-body tags gave, minus the late download start.
+ #expect(stylesheet.lowerBound < script.lowerBound)
+ #expect(script.lowerBound < content.lowerBound)
}
@Test