Skip to content

Commit bdfd028

Browse files
authored
[docs] Add SSR documentation (lit#880)
* Add Server Rendering section to docs * Add labs variant for asides * Add shortcode for labs-disclaimer
1 parent 094890b commit bdfd028

File tree

19 files changed

+461
-9
lines changed

19 files changed

+461
-9
lines changed

Diff for: packages/lit-dev-content/samples/tutorials/CONTRIBUTING.md

+1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ The available asides are:
230230
* `warn`
231231
* `negative`
232232
* `info`
233+
* `labs`
233234

234235
## Code Checking
235236

Diff for: packages/lit-dev-content/site/_includes/docs-nav-collapsing.html

+6
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@
1818
<span class="sectionHead
1919
{% if section.url == page.url %}active{% endif %}">
2020
{{ section.title }}
21+
{% if section.labs == true %}
22+
<img class="labs" src="/images/alerts/labs.svg" alt="labs" loading="lazy" fetchpriority="low" />
23+
{% endif %}
2124
</span>
2225
</summary>
2326
<ol>
2427
{%- for child in section.children %}
2528
<li{% if child.url == page.url %} class="active"{% endif %}>
2629
<a href="{{ child.url | url }}">{{ child.title }}</a>
30+
{% if child.labs == true %}
31+
<img class="labs" src="/images/alerts/labs.svg" alt="labs" loading="lazy" fetchpriority="low" />
32+
{% endif %}
2733
</li>
2834
{%- endfor %}
2935
</ol>

Diff for: packages/lit-dev-content/site/_includes/docs-nav.html

+8-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@
1414
<li{% if sectionActive %} class="activeSection"{% endif %}>
1515
{%- if section.children | length %}
1616
<span class="sectionHead{% if section.url == page.url %} active{% endif %}">
17-
{{ section.title }}</span>
17+
{{ section.title }}
18+
{% if section.labs == true %}
19+
<img class="labs" src="/images/alerts/labs.svg" alt="labs" loading="lazy" fetchpriority="low" />
20+
{% endif %}
21+
</span>
1822
<ol>
1923
{%- for child in section.children %}
2024
<li{% if child.url == page.url %} class="active"{% endif %}>
2125
<a href="{{ child.url | url }}">{{ child.title }}</a>
26+
{% if child.labs == true %}
27+
<img class="labs" src="/images/alerts/labs.svg" alt="labs" loading="lazy" fetchpriority="low" />
28+
{% endif %}
2229
</li>
2330
{%- endfor %}
2431
</ol>

Diff for: packages/lit-dev-content/site/css/docs.css

+11
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,10 @@ table.directory tr.subheading {
403403
color: red;
404404
}
405405

406+
.pre-release > summary {
407+
cursor: pointer;
408+
}
409+
406410
/* ------------------------------------
407411
* Prev/next links
408412
* ------------------------------------ */
@@ -549,6 +553,13 @@ table.directory tr.subheading {
549553
background: #d6d6d6;
550554
}
551555

556+
/* Labs icon */
557+
#docsNav img.labs {
558+
width: 1.25rem;
559+
height: 1.25rem;
560+
vertical-align: text-bottom;
561+
}
562+
552563
/* ------------------------------------
553564
* Inline TOC
554565
* ------------------------------------ */

Diff for: packages/lit-dev-content/site/css/mobile-nav.css

+7
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,10 @@ mwc-drawer:not(:defined) {
5151
#mobileDocsNav summary::marker {
5252
font-size: 13px;
5353
}
54+
55+
/* Labs icon */
56+
#mobileDocsNav img.labs {
57+
width: 1.25rem;
58+
height: 1.25rem;
59+
vertical-align: text-bottom;
60+
}

Diff for: packages/lit-dev-content/site/docs/api/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: API
33
eleventyNavigation:
44
title: API
55
key: API
6-
order: 7
6+
order: 8
77
---
88

99
<!-- This file exists only to create a section heading.

Diff for: packages/lit-dev-content/site/docs/libraries/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: Related libraries
33
eleventyNavigation:
44
key: Related libraries
5-
order: 9
5+
order: 10
66
versionLinks:
77
v1: lit-html/introduction/
88
---

Diff for: packages/lit-dev-content/site/docs/releases/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: Releases
33
eleventyNavigation:
44
key: Releases
5-
order: 8
5+
order: 9
66
---
77

88
<!-- This file exists only to create a section heading.

Diff for: packages/lit-dev-content/site/docs/resources/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: Resources
33
eleventyNavigation:
44
key: Resources
5-
order: 10
5+
order: 11
66
---
77

88
<!-- This file exists only to create a section heading.

Diff for: packages/lit-dev-content/site/docs/ssr/authoring.md

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
---
2+
title: Authoring components for Lit SSR
3+
eleventyNavigation:
4+
key: Authoring components
5+
parent: Server rendering
6+
order: 4
7+
---
8+
9+
{% labs-disclaimer %}
10+
11+
Lit's approach to rendering web components in a server environment places some restrictions on component code to achieve efficient server rendering. When authoring components, keep in mind these considerations to ensure they are compatible with Lit SSR.
12+
13+
Note: The restrictions listed on this page are subject to change as we make improvements to Lit SSR. If you would like to see a certain use case supported, please [file an issue](https://github.com/lit/lit/issues/new/choose) or start a [discussion](https://github.com/lit/lit/discussions) thread.
14+
15+
## Browser-only code
16+
17+
Most browser DOM APIs are not available in the Node environment. Lit SSR utilizes a DOM shim that's the bare minimum required for rendering Lit templates and components. For a full list of what APIs are available, see the [DOM Emulation](/docs/ssr/dom-emulation) page.
18+
19+
When authoring components, perform imperative DOM operations from lifecycle methods that are called **only on the client**, and not on the server. For example, use `updated()` if you need to measure the updated DOM. This callback is only run on the browser, so it is safe to access the DOM.
20+
21+
See the [lifecycle](#lifecycle) section below for a list of which specific methods are called on the server and which are browser-only.
22+
23+
Some modules that define Lit components may also have side effects that use browser APIs—for example to detect certain browser features—such that the module breaks when imported in a non-browser environment. In this case, you can move the side effect code into a browser-only lifecycle callback, or conditionalize so that it only runs on the browser.
24+
25+
For simple cases, adding conditionals or optional chaining to certain DOM accesses may be sufficient to guard against unavailable DOM APIs. For example:
26+
27+
```js
28+
const hasConstructableStylesheets = typeof globalThis.CSSStyleSheet?.prototype.replaceSync === 'function';
29+
```
30+
31+
The Lit team is also working on providing an easy way of checking whether the code is running in a Node environment which can be utilized to write conditional code blocks targeting different environments. Follow the issue [[labs/ssr] A way for client code to check if running in server (isSsr)](https://github.com/lit/lit/issues/3158) for updates on this feature.
32+
33+
For more complex uses cases, consider utilizing [conditional exports](https://nodejs.org/api/packages.html#conditional-exports) in Node that specifically match for `"node"` environments so you could have different code depending on whether the module is being imported for use in Node or in the browser. Users would get the appropriate version of the package depending on whether it was imported from Node or the browser. Export conditions are also supported by popular bundling tools like [rollup](https://github.com/rollup/plugins/tree/master/packages/node-resolve#exportconditions) and [webpack](https://webpack.js.org/configuration/resolve/#resolveconditionnames) so users can bring in the appropriate code for your bundle.
34+
35+
{% aside "warn" %}
36+
37+
Don't bundle Lit into published components.
38+
39+
Because Lit packages use conditional exports to provide different modules to Node and browser environments, we strongly discourage bundling `lit` into your packages being published to NPM. If you do, your bundle will only include `lit` modules meant for the environment you bundled, and won't automatically switch based on environment.
40+
41+
{% endaside %}
42+
43+
## Lifecycle
44+
45+
Only certain lifecycle callbacks are run during server-side rendering. These callback generate the initial styling and markup for the component. Additional lifecycle methods are called client-side during hydration and during runtime after the components are hydrated.
46+
47+
The tables below lists the standard custom element and Lit lifecycle methods and whether they are called during SSR. All of the lifecycle is available on the browser after element registration and hydration.
48+
49+
{% aside "warn" "no-header" %}
50+
51+
Methods called on the server should not contain references to browser/DOM APIs that have not been shimmed. Methods that are not called server-side may contain those references without causing breakages.
52+
53+
{% endaside %}
54+
55+
{% aside "labs" "no-header" %}
56+
57+
Whether a method is called on the server is subject to change while Lit SSR is part of Lit Labs.
58+
59+
{% endaside %}
60+
61+
<!-- TODO(augustinekim) Replace emoji with appropriate icon -->
62+
### Standard custom element and LitElement
63+
| Method | Called on server | Notes |
64+
|-|-|-|
65+
| `constructor()` | Yes ⚠️ | |
66+
| `connectedCallback()` | No | |
67+
| `disconnectedCallback()` | No | |
68+
| `attributeChangedCallback()` | No | |
69+
| `adoptedCallback()` | No | |
70+
| `hasChanged()` | Yes ⚠️ | Called when property is set |
71+
| `shouldUpdate()` | No | |
72+
| `willUpdate()` | Yes ⚠️ | Called before `render()` |
73+
| `update()` | No | |
74+
| `render()` | Yes ⚠️ | |
75+
| `firstUpdate()` | No | |
76+
| `updated()` | No | |
77+
78+
### ReactiveController
79+
| Method | Called on server | Notes |
80+
|-|-|-|
81+
| `constructor()` | Yes ⚠️ | |
82+
| `hostConnected()` | No | |
83+
| `hostDisconnected()` | No | |
84+
| `hostUpdate()` | No | |
85+
| `hostUpdated()` | No | |
86+
87+
### Directive
88+
| Method | Called on server | Notes |
89+
|-|-|-|
90+
| `constructor()` | Yes ⚠️ | |
91+
| `update()` | No | |
92+
| `render()` | Yes ⚠️ | |
93+
| `disconnected()` | No | Async directives only |
94+
| `reconnected()` | No | Async directives only |
95+
96+
## Asynchronicity
97+
98+
There currently isn't a mechanism to wait for asynchronous results before continuing to render (such as results from async directives or controllers), though we are considering options to allow this in the future. The current workaround for this is to do any asynchronous work before rendering the top level template on the server and providing the data to the template as some attribute or property.
99+
100+
For example:
101+
- Async directives such as `asyncAppend()` or `asyncReplace()` will not produce any renderable results server-side.
102+
- `until()` directive will only ever result in the highest-priority non-promise placeholder value.
103+
104+
## Testing
105+
106+
The `@lit-labs/testing` package contains utility functions that utilize a [Web Test Runner](https://modern-web.dev/docs/test-runner/overview/) plugin to create test fixtures that are rendered server-side using `@lit-labs/ssr`. It can help test whether your components are server-side renderable. See more in the [readme](https://github.com/lit/lit/tree/main/packages/labs/testing#readme).
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
title: Lit SSR client usage
3+
eleventyNavigation:
4+
key: Client usage
5+
parent: Server rendering
6+
order: 3
7+
---
8+
9+
{% labs-disclaimer %}
10+
11+
Lit SSR generates static HTML for the browser to parse and paint without any JavaScript. (Browsers that do not have support for Declarative Shadow DOM will require some JavaScript polyfill for Lit components authored to utilize the Shadow DOM.) For pages with static content, this is all that's needed. However, if the page content needs to be dynamic and respond to user interactions, it will need JavaScript to re-apply that reactivity.
12+
13+
How to re-apply that reactivity client-side depends on whether you are rendering standalone Lit templates or utilizing Lit components.
14+
15+
## Standalone Lit Templates
16+
"Hydration" for Lit templates is the process of having Lit re-associate the expressions of Lit templates with the nodes they should update in the DOM as well as adding event listeners. In order to hydrate Lit templates, the `hydrate()` method from the `experimental-hydrate` module is provided in the `lit` package. Before you update a server-rendered container using `render()`, you must first call `hydrate()` on that container using the same template and data that was used to render on the server:
17+
18+
```js
19+
import {render} from 'lit';
20+
import {hydrate} from 'lit/experimental-hydrate.js';
21+
import {myTemplate} from './my-template.js';
22+
// Initial hydration required before render:
23+
// (must be same data used to render on the server)
24+
const initialData = getInitialAppData();
25+
hydrate(myTemplate(initialData), document.body);
26+
27+
// After hydration, render will efficiently update the server-rendered DOM:
28+
const update = (data) => render(myTemplate(data), document.body);
29+
```
30+
31+
## Lit components
32+
To re-apply reactivity to Lit components, custom element definitions need to be loaded for them to upgrade, enabling their lifecycle callbacks, and the templates in the components' shadow roots needs to be hydrated.
33+
34+
Upgrading can be achieved simply by loading the component module that registers the custom element. This can be done by loading a bundle of all the component definitions for a page, or may be done based on more sophisticated heuristics where only a subset of definitions are loaded as needed. To ensure templates in `LitElement` shadow roots are hydrated, load the `lit/experimental-hydrate-support.js` module which installs support for `LitElement` to automatically hydrate itself when it detects it was server-rendered with declarative shadow DOM. This module must be loaded before the `lit` module is loaded (including any component modules that import `lit`) to ensure hydration support is properly installed.
35+
36+
When Lit components are server rendered, their shadow root contents are emitted inside a `<template shadowroot>`, also known as a [Declarative Shadow Root](https://web.dev/declarative-shadow-dom/). Declarative shadow roots automatically attach their contents to a shadow root on the template's parent element when HTML is parsed without the need for JavaScript.
37+
38+
Until all browsers include declarative shadow DOM support, a very small polyfill is available that can be inlined into your page. This lets you use SSR now for any browsers with JavaScript enabled and incrementally address non-JavaScript use cases as the feature is rolled out to other browsers. The usage of the [`template-shadowroot` polyfill](https://github.com/webcomponents/template-shadowroot) is described below.
39+
40+
### Loading `lit/experimental-hydrate-support.js`
41+
This needs to be loaded before any component modules and the `lit` library.
42+
43+
For example:
44+
```html
45+
<body>
46+
<!-- App components rendered with declarative shadow DOM placed here. -->
47+
48+
<!-- exprimental-hydrate-support should be loaded first. -->
49+
<script src="/node_modules/lit/exprimental-hydrate-support.js"></script>
50+
51+
<!-- As compnent definition loads, your pre-rendered components will
52+
come to life and become interactive. -->
53+
<script src="/app-components.js"></script>
54+
</body>
55+
```
56+
57+
If you are [bundling](/docs/tools/production/) your code, make sure the `lit/expriemntal-hydrate-support.js` is imported first:
58+
```js
59+
// index.js
60+
import 'lit/experimental-hydrate-support.js';
61+
import './app-components.js';
62+
```
63+
64+
### Using the `template-shadowroot` polyfill
65+
The HTML snippet below includes an optional strategy to hide the body until the polyfill is loaded to prevent layout shifts.
66+
67+
```html
68+
<!DOCTYPE html>
69+
<html>
70+
<head>
71+
<!-- On browsers that don't yet support native declarative shadow DOM, a
72+
paint can occur after some or all pre-rendered HTML has been parsed,
73+
but before the declarative shadow DOM polyfill has taken effect. This
74+
paint is undesirable because it won't include any component shadow DOM.
75+
To prevent layout shifts that can result from this render, we use a
76+
"dsd-pending" attribute to ensure we only paint after we know
77+
shadow DOM is active. -->
78+
<style>
79+
body[dsd-pending] {
80+
display: none;
81+
}
82+
</style>
83+
</head>
84+
85+
<body dsd-pending>
86+
<script>
87+
if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
88+
// This browser has native declarative shadow DOM support, so we can
89+
// allow painting immediately.
90+
document.body.removeAttribute('dsd-pending');
91+
}
92+
</script>
93+
94+
<!-- App components rendered with declarative shadow DOM placed here. -->
95+
96+
<!-- Use a type=module script so that we can use dynamic module imports.
97+
Note this pattern will not work in IE11. -->
98+
<script type="module">
99+
// Check if we require the template shadow root polyfill.
100+
if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
101+
// Fetch the template shadow root polyfill.
102+
const {hydrateShadowRoots} = await import(
103+
'/node_modules/@webcomponents/template-shadowroot/template-shadowroot.js'
104+
);
105+
106+
// Apply the polyfill. This is a one-shot operation, so it is important
107+
// it happens after all HTML has been parsed.
108+
hydrateShadowRoots(document.body);
109+
110+
// At this point, browsers without native declarative shadow DOM
111+
// support can paint the initial state of your components!
112+
document.body.removeAttribute('dsd-pending');
113+
}
114+
</script>
115+
</body>
116+
</html>
117+
```
118+
119+
### Combined example
120+
This example shows a strategy that combines both the `lit/experimental-hydrate-support.js` and the `template-shadowroot` polyfill loading and serves a page with a SSRed component to hydrate client-side.
121+
122+
[Lit SSR in a Koa server](https://stackblitz.com/edit/lit-ssr-global?file=src/server.js)
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
title: Lit SSR DOM emulation
3+
eleventyNavigation:
4+
key: DOM emulation
5+
parent: Server rendering
6+
order: 5
7+
---
8+
9+
{% labs-disclaimer %}
10+
11+
In the DOM shim that ships with Lit SSR, only the minimal DOM interfaces needed to define and register components are implemented. These include a few key DOM classes and a roughly functioning `CustomElementRegistry`.
12+
13+
Below lists all the properties, classes, and methods on the `window` object added to `globalThis`. The contents of `window` are also assigned onto `globalThis`. ✅ signifies item is implemented to be functionally the same as in the browser.
14+
15+
<!-- TODO(augustinekim) Consider replacing emojis below with icons https://github.com/lit/lit.dev/pull/880#discussion_r944821511 -->
16+
| Property | Notes |
17+
|-|-|
18+
| `Element` | ⚠️ Empty class |
19+
| `HTMLElement` | ⚠️ Partial <table><tbody><tr><td>`attributes`</td><td>✅</td><tr><td>`shadowRoot`</td><td>⚠️ Returns `{host: this}` if `attachShadow()` was called with `{mode: 'open'}`</td><tr><td>`setAttribute()`</td><td>✅</td><tr><td>`removeAttribute()`</td><td>✅</td><tr><td>`hasAttribute()`</td><td>✅</td><tr><td>`attachShadow()`</td><td>⚠️ Returns `{host: this}`</td><tr><td>`getAttribute()`</td><td>✅</td></tr></tbody></table> |
20+
| `Document` | ⚠️ Partial <table><tbody><tr><td>`adoptedStyleSheets`</td><td>⚠️ Getter only returning `[]`</td><tr><td>`createTreeWalker()`</td><td>⚠️ Returns `{}`</td><tr><td>`createTextNode()`</td><td>⚠️ Returns `{}`</td><tr><td>`createElement()`</td><td>⚠️ Returns `{}`</td></tr></tbody></table> |
21+
| `document` | Instance of `Document` |
22+
| `cssStyleSheet` | ⚠️ Partial <table><tbody><tr><td>`replace()`</td><td>⚠️ No op</td></tr></tbody></table> |
23+
| `ShadowRoot` | ⚠️ Empty class |
24+
| `CustomElementRegistry` | <table><tbody><tr><td>`define()`</td><td>✅</td></tr><tr><td>`get()`</td><td>✅</td></tr></tbody></table> |
25+
| `customElements` | Instance of `CustomElementRegistry` |
26+
| `btoa()` ||
27+
| `fetch()` | `node-fetch` |
28+
| `location` | `new URL('http://localhost')` |
29+
| `MutationObserver` | ⚠️ Partial <table><tbody><tr><td>`observe()`</td><td>⚠️ No op</td></tr></tbody></table> |
30+
| `requestAnimationFrame()` | ⚠️ No op |
31+
| `window` | ✅ Self reference |

Diff for: packages/lit-dev-content/site/docs/ssr/index.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: Server rendering
3+
eleventyNavigation:
4+
key: Server rendering
5+
order: 7
6+
labs: true
7+
---
8+
9+
<!-- This file exists only to create a section heading.
10+
Its output is deleted by the Eleventy build process. -->

0 commit comments

Comments
 (0)