npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

async-htm-to-string

v5.0.2

Published

Renders a htm tagged template asyncly into a string

Readme

async-htm-to-string

Renders a htm tagged template asyncly into a string.

npm version npm downloads Types in JS neostandard javascript style Ask DeepWiki Follow @voxpelli@mastodon.social

Usage

Simple

npm install async-htm-to-string
import { html, renderToString } from 'async-htm-to-string';

const customTag = ({ prefix }, children) => html`<div>${prefix}-${children}</div>`;
const dynamicContent = 'bar';
// Will equal "<div>foo-bar</div>"
const result = await renderToString(html`<${customTag} prefix="foo">${dynamicContent}</${customTag}>`);

Async Support

The library has full support for async values:

  • Async Components: Components can be async functions
  • Async Children: Children can be Promises or arrays of Promises
  • Deeply Nested: Resolved values are recursively processed
  • Concurrency: Uses buffered-async-iterable to process async arrays concurrently while maintaining order
const AsyncComponent = async ({ id }) => {
  const data = await fetchData(id);
  return html`<div>${data}</div>`;
};

// <AsyncComponent /> will be awaited and rendered
const result = await renderToString(html`
  <${AsyncComponent} id="123" />
  ${Promise.resolve('Async child')}
`);

API

html

Is h() bound to htm (htm.bind(h)). Used with template literals, like:

const renderableElement = html`<div>${content}</div>`;

rawHtml / rawHtml(rawString)

If you need to provide pre-escaped raw HTML content, then you can use rawHtml as either a template literal or by calling it with a string.

Security Warning: Never pass user-controlled or untrusted content to rawHtml. It bypasses HTML escaping and can lead to XSS if used with untrusted input.

const renderableElement = rawHtml`<div>&amp;${'&quot;'}</div>`;
const renderableElement = rawHtml('<div>&amp;</div>');

You can also use the result of any of those rawHtml inside html, like:

const renderableElement = html`<div>${rawHtml`&amp;`}</div>`;

h(type, props, ...children)

The inner method that's htm is bound to.

render(renderableElement)

Takes the output from html and returns an async iterator that yields the strings as they are rendered.

renderToString(renderableElement)

Same as render(), but asyncly returns a single string with the fully rendered result, rather than an async iterator. Automatically uses a synchronous fast path for pure-HTML templates (no async components or Promise children), avoiding async generator overhead entirely.

renderToStringSync(renderableElement)

Synchronous version of renderToString() that returns a string directly instead of a Promise<string>. Throws a TypeError if the input is a Promise, or an Error if an async component is encountered during rendering.

Best suited for templates known to be fully synchronous:

import { html, renderToStringSync } from 'async-htm-to-string';

// Returns string directly, no await needed
const result = renderToStringSync(html`<div class="fast">Hello</div>`);

Performance

Templates built entirely from string tags and static content (no async components or Promise children) are automatically detected and rendered via a synchronous fast path. This avoids creating async generators and Promises, providing significant speedups for sync-heavy workloads.

The optimization works at multiple levels:

  • Element creation: h() marks elements with async: false when the type is a string and all children are sync primitives or other sync elements
  • Sync fast path in renderToString(): Elements with async: false bypass async generators entirely, using direct string concatenation
  • Hybrid optimization: Even in async renders, sync subtrees returned by function components are rendered via the fast path
  • isPromise guards: Async generators skip await on values that are already resolved

Benchmark results

Measured with mitata on Node.js 22 (Apple M1), with --expose-gc and .gc('inner') for consistent GC behavior. "Legacy" is the pre-optimization async generator path. See benchmark.js for the full source.

| Template | Legacy | renderToString | renderToStringSync | |---|---|---|---| | Simple (<div>Hello</div>) | 57 µs | 465 ns (123x faster) | 360 ns (158x faster) | | Medium (nested HTML, props, lists) | 165 µs | 3.9 µs (43x faster) | 3.1 µs (54x faster) | | rawHtml child | 7.3 µs | 674 ns (11x faster) | 568 ns (13x faster) | | Sync function component | 1.42 µs | 1.48 µs (1.0x) | 769 ns (1.8x faster) | | Async function component | 1.45 µs | 1.79 µs | n/a |

Key takeaways:

  • Pure HTML templates are 40-160x faster than the legacy async generator path. The previous approach created ~9 async generators and 20-30 Promises for even a trivial <div>Hello</div> — all resolved synchronously but each requiring a microtask tick.
  • renderToString() benefits automatically — no code changes needed. It detects sync trees and takes the fast path.
  • renderToStringSync() adds ~20% more on top by avoiding even the outer async wrapper and its single microtask.
  • Sync function components benefit from renderToStringSync (1.8x) but not from the auto fast path in renderToString, since function components prevent top-level async: false detection. The hybrid optimization does kick in for sync subtrees within async renders.
  • No regression for async content — templates with async components or Promise children use the same async generator path as before.

Why these numbers are so large

The dramatic speedups are consistent with findings across the Node.js ecosystem:

  • Async generator overhead is well-documented. Each yield in an async function* allocates a Promise and schedules a microtask. V8's async function internals show that even optimized await requires microtask scheduling. Node.js issue #31979 documents ~10x slowdowns for for await...of vs for...of on the same data.
  • Sync fast paths are an established pattern. Cloudflare's streams research shows up to 10x speedups from eliminating promises in sync code paths. Preact, Solid.js, and Lit SSR all offer explicit sync rendering modes for the same reason.
  • The overhead compounds. A simple <div>Hello</div> previously created ~9 async generators, ~20-30 Promises, and scheduled ~20-30 microtask ticks — all to concatenate 3 strings. The sync path does this in a single function call with zero allocations.

Run the benchmark yourself with npm run benchmark.

Security considerations

This library escapes interpolated values — text content and attribute values are HTML-escaped (the 6 characters `& < > " ' ``). This protects against XSS when rendering user-provided strings:

const userInput = '<script>alert("xss")</script>';
// Safe: renders as &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;
await renderToString(html`<div>${userInput}</div>`);

However, this library is a renderer, not a sanitizer:

  • Tag and attribute names are not restricted. Tags like script, iframe etc. and attributes like onclick are valid — the library needs to support all tag names and attributes to enable eg. custom elements and future web platform additions.
  • rawHtml bypasses all escaping. Never pass untrusted input to rawHtml.
  • Only the 6 HTML-dangerous characters are escaped. Control characters and non-ASCII content (emoji, CJK, accented characters, etc.) are passed through as-is — these characters cannot break HTML structure, so escaping them would corrupt valid content.

Helpers

generatorToString(somethingIterable)

Asyncly loops over an iterable (like eg. an async iterable) and concatenates together the result into a single string that it resolves to. The brains behind renderToString().