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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@popeindustries/lit-html-server

v6.1.3

Published

Efficiently render streaming lit-html templates on the server (or in a ServiceWorker!)

Downloads

6,902

Readme

NPM Version

@popeindustries/lit-html-server

Efficiently render streaming lit-html templates on the server (or in a ServiceWorker!).

Features

  • 6-7x faster than @lit-labs/ssr
  • render full HTML pages (not just <body>)
  • stream responses in Node.js and ServiceWorker, with first-class Promise and AsyncIterator support
  • render optional hydration metadata with hydratable directive
  • render web components with light or shadow DOM
  • default web component rendering with element.innerHTML and element.render() support
  • customisable web component rendering with ElementRenderer
  • compatible with lit-html/directives/*

Usage

Install with npm/yarn/pnpm:

$ npm install --save @popeindustries/lit-html-server

...write your lit-html template:

import { html } from '@popeindustries/lit-html-server';
// Most lit-html directives are compatible...
import { classMap } from 'lit-html/directives/class-map.js';
// ...except for the async ones ('async-append', 'async-replace', and 'until')
import { until } from '@popeindustries/lit-html-server/directives/until.js';

function Layout(data) {
  return html`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>${data.title}</title>
      </head>
      <body>
        ${until(renderBody(data.api))}
      </body>
    </html>
  `;
}

async function renderBody(api) {
  // Some Promise-based request method
  const data = await fetchRemoteData(api);

  return html`
    <h1>${data.title}</h1>
    <my-el ?enabled="${data.hasWidget}"></my-el>
    <p class="${classMap({ negative: data.invertedText })}">${data.text}</p>
  `;
}

...and render (plain HTTP server example, though similar for Express/Fastify/etc):

import http from 'node:http';
import { renderToNodeStream } from '@popeindustries/lit-html-server';

http.createServer((request, response) => {
  const data = { title: 'Home', api: '/api/home' };
  response.writeHead(200);
  // Returns a Node.js Readable stream which can be piped to "response"
  renderToNodeStream(Layout(data)).pipe(response);
});

Hydration

Server rendered HTML may be converted to live lit-html templates with the help of inline metadata. This process of reusing static HTML to seamlessly bootstrap dynamic templates is referred to as hydration.

lit-html-server does not output hydration metadata by default, but instead requires that a sub-tree is designated as hydratable via the hydratable directive:

import { hydratable } from '@popeindustries/lit-html-server/directives/hydratable.js';

function Layout(data) {
  return html`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>${data.title}</title>
      </head>
      <body>
        <h1>Some ${data.title}</h1>
        ${hydratable(renderMenu(data.api))}
        <p>
          Some paragraph of text to show that multiple<br />
          hydration sub-trees can exist in the same container.
        </p>
        ${hydratable(renderPage(data.api))}
        <footer>Some footer</footer>
      </body>
    </html>
  `;
}

...which generates output similar to:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    <h1>Some Title</h1>
    <!--lit qKZ2lAadfCg=-->
    <nav negative>
      <!--lit-attr 1--><!--lit-child--><!--lit-child zRvOSEJDeXc=--><button><!--lit-child-->one<!--/lit-child--></button
      ><!--/lit-child--><!--lit-child zRvOSEJDeXc=--><button><!--lit-child-->two<!--/lit-child--></button
      ><!--/lit-child--><!--lit-child zRvOSEJDeXc=--><button><!--lit-child-->three<!--/lit-child--></button
      ><!--/lit-child--><!--/lit-child-->
    </nav>
    <!--/lit-->
    <p>
      Some paragraph of text to show that multiple<br />
      hydration sub-trees can exist in the same container.
    </p>
    <!--lit 83OJYYYBUzs=-->
    <main>This is the main page content.</main>
    <!--/lit-->
    <footer>Some footer</footer>
  </body>
</html>

In order to efficiently reuse templates on the client (renderMenu and renderPage in the example above), they should be hydrated and rendered with the help of @popeindustries/lit-html.

Web Components

The rendering of web component content is largely handled by custom ElementRenderer instances that adhere to the following interface:

declare class ElementRenderer {
  /**
   * Should return true when given custom element class and/or tag name
   * should be handled by this renderer.
   */
  static matchesClass(ceClass: typeof HTMLElement, tagName: string): boolean;
  /**
   * The custom element instance
   */
  readonly element: HTMLElement;
  /**
   * The custom element tag name
   */
  readonly tagName: string;
  /**
   * The element's observed attributes
   */
  readonly observedAttributes: Array<string>;
  /**
   * Constructor
   */
  constructor(tagName: string);
  /**
   * Function called when element is to be rendered
   */
  connectedCallback(): void;
  /**
   * Function called when observed element attribute value has changed
   */
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
  /**
   * Update element property value
   */
  setProperty(name: string, value: unknown): void;
  /**
   * Update element attribute value
   */
  setAttribute(name: string, value: string): void;
  /**
   * Render element attributes as string
   */
  renderAttributes(): string;
  /**
   * Render element styles as string for applying to shadow DOM
   */
  renderStyles(): string;
  /**
   * Render element content
   */
  render(): TemplateResult | string | null | undefined;
}

Custom ElementRenderer instances should subclass the default renderer, and be passed along to the render function:

import { renderToNodeStream } from '@popeindustries/lit-html-server';
import { ElementRenderer } from '@popeindustries/lit-html-server/element-renderer.js';

class MyElementRenderer extends ElementRenderer {
  static matchesClass(ceClass, tagName) {
    return '__myElementIdentifier__' in ceClass;
  }

  render() {
    return this.element.myElementRenderFn();
  }
}

const stream = renderToNodeStream(Layout(data), {
  elementRenderers: [MyElementRenderer],
});

Note the default ElementRenderer will render innerHTML strings, or content returned by this.element.render(), in either light or shadow DOM.

See @popeindustries/lit-element for LitElement support.

Shadow DOM

If attachShadow() has been called by an element during construction/connection, lit-html-server will render the custom element content in a declarative Shadow DOM:

<!--lit Ph5bNbG/om0=-->
<my-el>
  <!--lit-attr 0-->
  <template shadowroot="open"> <!--lit iW9ZALRtWQA=-->text<!--/lit--> </template>
</my-el>
<!--/lit-->

Disabling server render

For web components that will only be rendered on the client, add the render:client attribute to disable server-rendering for that component:

html`<my-el render:client><span slot="my-text">some text</span></my-el>`;

Lazy (partial/deferred) hydration

When rendering web components, lit-html-server adds hydrate:defer attributes to nested custom elements. This provides a mechanism to control and defer the hydration order of components that may be dependant on data passed from a parent. See lazy-hydration-mixin for more on lazy hydration.

DOM polyfills

In order to support importing and evaluating custom element code in Node, minimal DOM polyfills are attached to the Node global when @popeindustries/lit-html-server is imported. See dom-shim.js for details.

Directives

Most of the built-in lit-html/directives/* already support server-rendering, and work as expected in lit-html-server, the exception being those directives that are asynchronous. lit-html-server supports the rendering of Promise and AsyncInterator as first-class primitives, so versions of async-append.js, async-replace.js, and until.js should be imported from @popeindustries/lit-html-server/directives.

Benchmarks

Benchmarks for rendering a complex template in lit-html-server vs. @lit-labs/ssr:

# @popeindustries/lit-html-server
$ node ./benchmark/perf.js
┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max    │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
│ Latency │ 381 ms │ 541 ms │ 553 ms │ 588 ms │ 509.52 ms │ 66.97 ms │ 761 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘
┌───────────┬────────┬────────┬────────┬────────┬─────────┬─────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg     │ Stdev   │ Min    │
├───────────┼────────┼────────┼────────┼────────┼─────────┼─────────┼────────┤
│ Req/Sec   │ 7939   │ 7939   │ 9207   │ 9327   │ 9092.55 │ 370.06  │ 7938   │
├───────────┼────────┼────────┼────────┼────────┼─────────┼─────────┼────────┤
│ Bytes/Sec │ 150 MB │ 150 MB │ 174 MB │ 175 MB │ 172 MB  │ 6.89 MB │ 150 MB │
└───────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴────────┘
# @lit-labs/ssr
$ node ./benchmark/perf.js ssr
┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐
│ Stat    │ 2.5%   │ 50%     │ 97.5%   │ 99%     │ Avg        │ Stdev      │ Max     │
├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤
│ Latency │ 633 ms │ 4605 ms │ 6353 ms │ 6588 ms │ 3987.46 ms │ 1641.11 ms │ 7517 ms │
└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Req/Sec   │ 975     │ 975     │ 1280    │ 1581    │ 1322.7  │ 165.19 │ 975     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Bytes/Sec │ 20.5 MB │ 20.5 MB │ 26.8 MB │ 33.8 MB │ 27.9 MB │ 3.6 MB │ 20.5 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────┴─────────┘

(Results from local run on 2022 Macbook Air with [email protected])

API

RenderOptions

The following render methods accept an options object with the following properties:

  • elementRenderers?: Array<ElementRendererConstructor> - ElementRenderer subclasses for rendering of custom elements.

renderToNodeStream(value: unknown, options?: RenderOptions): Readable

Returns the value (generally the result of a template tagged by html) as a Node.js Readable stream of markup:

import { html, renderToNodeStream } from '@popeindustries/lit-html-server';

const name = 'Bob';
renderToNodeStream(html`<h1>Hello ${name}!</h1>`).pipe(response);

renderToWebStream(value: unknown, options?: RenderOptions): ReadableStream

Returns the value (generally the result of a template tagged by html) as a web ReadableStream stream of markup:

import { html, renderToWebStream } from '@popeindustries/lit-html-server';

self.addEventListener('fetch', (event) => {
  const name = 'Bob';
  const stream = renderToWebStream(html`<h1>Hello ${name}!</h1>`);
  const response = new Response(stream, {
    headers: {
      'content-type': 'text/html',
    },
  });

  event.respondWith(response);
});

Note: due to the slight differences when running in Node or the browser, a separate version for running in a browser environment is exported as @popeindustries/lit-html-server/lit-html-service-worker.js. For those dev servers/bundlers that support conditional package.json#exports, exports are provided to enable importing directly from @popeindustries/lit-html-server.

renderToString(value: unknown, options?: RenderOptions): Promise<string>

Returns the value (generally the result of a template tagged by html) as a Promise which resolves to a string of markup:

import { html, renderToString } from '@popeindustries/lit-html-server';

const name = 'Bob';
const markup = await renderToString(html` <h1>Hello ${name}!</h1> `);
response.end(markup);

renderToBuffer(value: unknown, options?: RenderOptions): Promise<Buffer>

Returns the value (generally the result of a template tagged by html) as a Promise which resolves to a Buffer of markup:

import { html, renderToBuffer } from '@popeindustries/lit-html-server';

const name = 'Bob';
const markup = await renderToBuffer(html` <h1>Hello ${name}!</h1> `);
response.end(markup);