New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@popeindustries/lit-html-server

Package Overview
Dependencies
Maintainers
2
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@popeindustries/lit-html-server

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

  • 6.1.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
1.8K
decreased by-10.14%
Maintainers
2
Weekly downloads
 
Created
Source

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 Node@19.4.0)

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);

Keywords

FAQs

Package last updated on 21 Jun 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc