Async/await and @resultBuilder: Am I in danger?

I just merged a PR in my HTML package elementary that I think is really cool - but I am a bit scared...

My goal was to find a way to support awaiting stuff inside an element's @resultBuilder "body", as that would make for quite a nice server feature (ie: start streaming HTML down the pipe while some nested part of a page is still fetching).

I naively just added @escaping @Sendable ... async to an existing result builder closure - honestly not expecting it to work at all - but it just did. I am having a hard time fully grasping what compiler wizardry is going on here, but things seem to magically just work.

On the forums I only found very few posts (and rather old ones) about this combination (some vaguely mentioning issues with this), so my question basically is:

How close to the edge am I driving here?
Are we in "totally fully supported no big deal", or rather in "don't look at it too fast or it'll fall apart" territory?
Does anyone have experience with this combination?

Here is a bit of code to show what I am scared of:

public struct AsyncContent<Content: HTML>: HTML, Sendable {
    var content: @Sendable () async throws -> Content

    public init(@HTMLBuilder content: @escaping @Sendable () async throws -> Content) {
        self.content = content
    }

    @_spi(Rendering)
    public static func _render<Renderer: _AsyncHTMLRendering>(_ html: consuming Self, into renderer: inout Renderer, with context: consuming _RenderingContext) async throws {
        try await Content._render(await html.content(), into: &renderer, with: context)
    }
}
// all HTML elements have an initializer overload that auto-wraps an async closure in this AsyncContent type
div {
    let text = await getMyData()
    p { "This totally works: \(text)" }
    MyComponent()
}

struct MyComponent: HTML {
    var content: some HTML {
        AsyncContent {
            "So does this: \(await getMoreData())"
        }
    }
}

Sorry for a bit side question, that sounds cool, yet I struggle to understand how in the current implementation this works? From code perspective it looks like it is going to execute sequentially anyway and await for all loading (which also is going to happen one by one). Am I missing something or this part just not ready?

It is mostly for ergonomics, not so much performance.

However, by default, the HTML is streamed into some channel, so all the chunks up to the first await are potentially already on the wire - the full result is never awaited and never really stored anywhere.

Especially for larger pages with a lot of CSS or JS links at the top this would still help with the overall page load performance (as the browser can start fetching those in parallel).

I intend to add an AsyncForEach element as well that takes an AsyncSequence and maps it to HTML on the fly as well. At the very least I expect this to be good for memory consumption, since I do not need to first await/collect all the data, only to the render and stream it. I may be wrong, but in my brain it makes sense, as you only hold on to data for the tiny amount you need to spit out HTML bytes and be done with it.

I can also see a future where there is explicit "flush" control maybe, and even a "parallelize rendering" mode (but that would involve buffering sub-parts and then stream them out in the correct order again.... not sure about this one...)

1 Like