From c603421c28858701c137ef1032e17c69c573d82e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Aug 2026 12:06:47 +0200 Subject: [PATCH] Implemented the scripts' deferral on the Page protocol in the Infrastructure package. --- .../Sources/Public/Protocols/Page.swift | 31 ++++++++++++------- .../Cases/Public/Protocols/PageTests.swift | 12 ++++--- 2 files changed, 27 insertions(+), 16 deletions(-) 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