There’s a new web platform feature flighting in Chromium called Declarative Partial Updates. This proposal adds a new <?marker> element which enables out-of-order streaming of content. Rather than the traditional way of building the HTML you need on the server, or sending JSON down to a JavaScript framework, you can staple HTML onto the initial response and use the <template for=""> syntax to inject that content into the <?marker name=""> placeholder.

A simple example of this would be:

<!-- NOTE:
FYI, throughout this post I'm using the closing `?>` 
syntax for `<?marker>` becuase the HTML highlighter 
likes that better. Either syntax works.
-->
<main id="main">
  <?marker name="post-abc123" ?>
</main>

<!-- ...bunch of html goes here... -->

<template for="post-abc123">
	<div class="post">
		<h2>Hello world</h2>
		<div>First post</div>
	</div>
</template>

The late-arriving template will auto-magically get slurped up into the <?marker> placeholder slot. Unlike the <slot> element though, once you use <?marker> it’s done. You burned that card and it doesn’t work a second time. If you want to chain more than one post together and create a feed of posts, you can repeat the placeholder with the same <?marker name="post-abc123"> at the bottom of the appended item.

The essence of the feature is that you can open a server connection, never close it, and keep pumping more HTML onto the page. The <?marker> element does the heavy lifting of taking that new HTML and putting it in the right place. There’s lots of potential here (e.g. my beloved HTML Imports?) but I think the canonical use case would be how an LLM chatbot could stream in content that generates at different speeds:

<div class="chat-response">
	<?marker name="chat-response-abc123-widget"?>
	
	Markdown formatted text goes here...
</div>

<template for="chat-response-abc123-widget">
Hi, sorry I took so long! Here's a whole-ass application.
</template>

That will create some other problems like FOUC and LCP hurk-jerk you will have to manage –and that’s for another post– but lazy-loaded HTML rendering adds an interesting capability to the web platform. It took me a couple walks around the neighborhood to understand how I could potentially use <?marker> and out-of-order rendering on a normal workday.

Rendering a threaded response

The rule of thumb I came up with is that <?marker> might be a pattern to reach for any time where you want important content to show up first and then you kind of don’t care when other content shows up below the fold.

  • A post with nested replies
  • A product with reviews that show reviewer profile cards
  • A gigantic pull request with nested comment threads
  • A book with authors that have related books

You get the gist, a classic relational data situation. The example I’m going to go with is a nested reply thread like a Reddit thread or Bluesky post.

Building a nested thread of replies isn’t rocket science, but you have to do a lot of for-looping to first build up the tree of posts you’re going to render…

→ Fetch all root posts in thread
  → Loop through posts
    → Fetch replies for each root post
      → Loop through root post replies
        → Fetch replies for each reply
          → Add nested reply to parent reply
            → Add parent replies to root posts
		          → Walk tree
			          → Send HTML for all posts

Assuming the data tree gets stitched together by the database or our server backend, your first attempt at rendering nested content will probably be the most literal approach: create a for-loop for each level of content…

// pseudo-code
const postsTree = postsDb.find(p => p.thread_id === 'abc123');

postsTree.map(post => 
	html`<div id="post-${post.id}" class="post">
		<div>${post.username}</div>
		<div>${post.body}</div>
	
		${post.replies?.map(reply => 
			html`<div id="post-${reply.id}" class="post">
				<div>${reply.username}</div>
				<div>${reply.body}</div>
			
				${reply.replies?.map(nestedReply => 
					html`<div id="post-${nestedReply.id}" class="post">
						<div>${nestedReply.username}</div>
						<div>${nestedReply.body}</div>
					</div>`
				).join('')}
			</div>`
		).join('')}
	</div>`
).join('')

An obvious limitation here is that we’ve hard-coded ourselves to three levels of nesting. Adding more nesting would require backend and frontend work and break the classic “Rule of three” programming rule. Hard-coding your nesting is the “dumb way” to render nested content… but also the normal way.

Making this a bit smarter and more infinite

Philosophically-speaking, UI should be agnostic to the content it renders. If nested content is a possibility, a content renderer should theoretically support infinite nested content. Practically-speaking, most of us have learned that it doesn’t matter if the Product Manager’s Jira ticket says “The max is two nested items”, there will always be at least three.

The smarter way is to render this is to use recursion because nested for-loops are BAD and you are DUMB if you use them. But if you obfuscate the nesting into a function that calls itself, then that’s ✨recursion, baby✨ and you’re a GENIUS programmer.

Jokes aside, recursion will clean up the template code and simplify getting HTML on the page.

→ Fetch all posts in thread
  → Loop through posts
	  → Build a map
      → Loop through map
	      → Build a tree
          → Walk tree
            → Send HTML for all posts

Before we grabbed all posts, then all replies, then all nested replies. Walking that tree is a bit slow. We can make that faster by fetching a flat list of posts in the thread and then assemble a tree. You can do this with some crafty LEFT JOIN SQL statements but its generally easier on the server.

buildPostTree: building a nested tree from a flat list of posts
function buildPostTree(posts) {
	const postsById = new Map()
	// Loop through posts to build map
	for (const post of posts) {
		postsById.set(post.id, { ...post, replies: [], })
	}
	
	const roots = []
	
	// Loop through map values to build tree
	for (const post of postsById.values()) {
		// Parentless posts must be roots
		if (!post.parent_id) { roots.push(post); continue; }
	
		// Find this child's parents!
		const parent = postsById.get(post.parent_id)
		// Add child post to parent's replies
		parent.replies.push(post)
	}
	return roots
}

Then instead of hard-coding nested for-loops, we can recursively walk the tree to render the HTML for the posts.

// pseudo-code
const posts = postsDb.find(p => p.thread_id === 'abc123');
const postsTree = buildPostsTree(posts);

const renderPost = (post) => html`
	<div id="post-${post.id}" class="post">
		<div>${post.username}</div>
		<div>${post.body}</div>
	
		${
      // Loop through post replies and render the HTML
      post.replies?.map(reply => renderPost(reply)).join('')
    }
	</div>
`;

postsTree.map(post => renderPost(post)).join('')

That’s a more robust and understandable way to handle infinitely nested content. Our code is more durable to future changes. Need more or less nesting? Sure, we don’t care. That’s a data concern not a component rendering concern.

Even though we’ve simplified and reduced lines of code, both strategies have the same bottleneck: You need to build the entire tree before you can render the HTML.

The user waits for us to stitch the data together and the user waits for us to walk the tree and render synchronously. That’s two render blocking operations to put a thread onto the page.

The <?marker> element gives us another path…

Infinitely nested threads with out-of-order HTML

First we setup our page to receive the HTML, like we did at the top of this post…

<main id="main">
  <?marker name="post-abc123" ?>
</main>

And our rendering strategy gets a lot more simple…

→ Get all posts in thread
  → Loop through posts
    → Send HTML for post

And code looks a little something like this…

/* get all root posts */
const posts = postsDb.find(p => p.thread_id === 'abc123');

const renderPost = (post) => html`
	<template for="post-${post.parent_id ?? post.thread_id}">
		<div id="post-${post.id}" class="post">
			<div>${post.username}</div>
			<div>${post.body}</div>
		
			<?marker name="post-${id}">
		</div>
		<?marker name="post-${parent_id ?? post.thread_id}">
	</template>
`

/* loop through posts */
posts.forEach(post => 
	main.appendHTML(renderPost(post))
);

No tree building is happening whatsoever on the server. The microsecond after we generate the template we’re able to stream/send the HTML directly to the client and the page assembles itself. No recursion, no pre-assembly required. It all snap-fits together without us imperatively injecting content.

See the Pen LEFT JOIN with HTML <?marker> by Dave Rupert (@davatron5000) on CodePen.

It’s worth noting a thread of 10K posts will crush the browser, so you’ll still have to load responsibly and think about “Show more comments” buttons. I’m hand-waving the details here, but this gets easier too because your loadMoreComments() function doesn’t need to know anything about where to append those new comments, it can just staple them to the bottom of the page.

I’m excited about the possibilities here of removing ancillary content or UI from the critical rendering path. I’m interested in HTML’s newfound ability to self-assemble content and relationships in a declarative way. And maybe… he says with a glimmer of hope in his eyes… HTML Imports will be resurrected and there will be peace on Earth.