Efficiently render streaming lit-html templates on the server (or in a ServiceWorker!)
npm install @popeindustries/lit-html-server
Efficiently render streaming lit-html templates on the server (or in a ServiceWorker!).
- 6-7x faster than @lit-labs/ssr
- render full HTML pages (not just )
- 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/*
Install with npm/yarn/pnpm:
``bash`
$ npm install --save @popeindustries/lit-html-server
...write your lit-html template:
`js
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
;
}async function renderBody(api) {
// Some Promise-based request method
const data = await fetchRemoteData(api);
return html
${data.text}
;
}
`...and render (plain HTTP server example, though similar for Express/Fastify/etc):
`js
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:`js
import { hydratable } from '@popeindustries/lit-html-server/directives/hydratable.js';function Layout(data) {
return html
Some paragraph of text to show that multiple
hydration sub-trees can exist in the same container.
;
}
`...which generates output similar to:
`html
Title
Some Title
Some paragraph of text to show that multiple
hydration sub-trees can exist in the same container.
This is the main page content.
`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:`ts
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;
/**
* 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:`js
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.$3
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:`html
text
`$3
For web components that will only be rendered on the client, add the
render:client attribute to disable server-rendering for that component:`js
html;
`$3
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.$3
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:
`bash
@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 │
└───────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴────────┘
``bash
@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
####
RenderOptionsThe following render methods accept an
options object with the following properties:-
elementRenderers?: Array - ElementRenderer subclasses for rendering of custom elements.####
renderToNodeStream(value: unknown, options?: RenderOptions): ReadableReturns the
value (generally the result of a template tagged by html) as a Node.js Readable stream of markup:`js
import { html, renderToNodeStream } from '@popeindustries/lit-html-server';const name = 'Bob';
renderToNodeStream(html
).pipe(response);
`####
renderToWebStream(value: unknown, options?: RenderOptions): ReadableStreamReturns the
value (generally the result of a template tagged by html) as a web ReadableStream stream of markup:`js
import { html, renderToWebStream } from '@popeindustries/lit-html-server';self.addEventListener('fetch', (event) => {
const name = 'Bob';
const stream = renderToWebStream(html
);
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.$3
Returns the
value (generally the result of a template tagged by html) as a Promise which resolves to a string of markup:`js
import { html, renderToString } from '@popeindustries/lit-html-server';const name = 'Bob';
const markup = await renderToString(html
);
response.end(markup);
`$3
Returns the
value (generally the result of a template tagged by html) as a Promise which resolves to a Buffer of markup:`js
import { html, renderToBuffer } from '@popeindustries/lit-html-server';const name = 'Bob';
const markup = await renderToBuffer(html
);
response.end(markup);
``