| title | status | created_at | updated_at | pr |
|---|---|---|---|---|
LWC server runtime |
DRAFTED |
2020-01-06 |
2020-01-06 |
(leave this empty until the PR is created) |
The LWC engine has been designed from the beginning to run in an environment with access to the DOM APIs. To accommodate server-side rendering (SSR) requirements, we need a way to decouple the engine from the DOM APIs. The existing @lwc/engine will be replaced by 3 new packages:
@lwc/engine-coreexposes platform-agnostic APIs and will be used by the different runtime packages to share the common logic.@lwc/engine-domexposes LWC APIs available on the browser.@lwc/engine-serverexposes LWC APIs used for server-side rendering.
LWC SSR support is a broad subject, this proposal only focuses on a subset of it. This proposal covers the following topics:
- How to evaluate the existing LWC engine module in a javascript context without DOM access.
- What LWC APIs should be exposed in both environments.
- How the LWC engine share the same logic when evaluated in both environments.
The following topics are considered out of the scope of this proposal:
- How the LWC component tree rendered on the server gets serialized to HTML and sent to the browser.
- How the serialized component tree gets rehydrated when processed by the browser.
The first step in enabling SSR support on LWC is to be able to run the engine in a non-DOM-enabled environment. It is currently impossible to evaluate the engine in such environment due to its hard dependencies on DOM APIs.
While we want to share as much logic as possible between the different environments and make SSR as transparent as possible for component authors, the runtime behavior and APIs exposed by the engine greatly differs between environments. In this regard, it makes sense to break up the current @lwc/engine package into multiple packages specifically tailored for each environment.
Distributing multiple versions of the LWC engine offers the capability to expose different APIs depending on the execution context. On one hand, the current implementation of createElement with reaction hooks doesn't make sense in Node.js since there is no DOM tree to attach the created element. In the same way, the renderToString method doesn't make sense to ship in a browser.
The existing @lwc/engine will be replaced by 3 new packages:
@lwc/engine-core: Core logic shared by the different runtimes including the rendering engine and the reactivity mechanism. This package should never be consumed directly in an application. This package is agnostic on the underlying rendering medium. It only provides APIs for building custom runtimes.@lwc/engine-dom: Runtime that can be used to render LWC component trees in a DOM environment. This package is built on top of@lwc/engine-core.@lwc/engine-server: Runtime used that can be used to render LWC component trees as strings. This package is built on top of@lwc/engine-core.
The existing lwc package stays untouched and will be used to distribute the different versions of the engine. From the developer perspective, the experience should be identical.
c/app/app.js:
import { LightningElement } from "lwc";
export default class App extends LightningElement {}client.js:
import { createElement } from "lwc"; // Aliased to @lwc/engine-dom
import App from "c/app";
const app = createElement("c-app", { is: App });
document.body.appendChild(app);server.js:
import { createElement, renderToString } from "lwc"; // Aliased to @lwc/engine-server
import App from "c/app";
const app = createElement("c-app", { is: App });
const str = renderToString(app);
console.log(str);This packages exposes the following platform-agnostic APIs:
LightningElementapitrackreadonlywiresetFeatureFlaggetComponentDefisComponentConstructorgetComponentConstructorunwrapregisterTemplateregisterComponentregisterDecoratorssanitizeAttribute
The DOM APIs used by the rendered engine are injected by the runtime depending on the environment. A list of all the DOM APIs the engine depends upon can be found in the DOM APIs usage section in the Appendix.
This package exposes the following APIs:
createElement+ reaction hooksbuildCustomElementConstructorisNodeFromTemplate
This package injects the native DOM APIs into the @lwc/engine-core rendering engine.
This package exposes the following APIs:
createElement(name: string, options: { is: typeof LightningElement }): ServerHTMLElement: This method creates a new LWC component tree. It follows the same signature as thecreateElementAPI from@lwc/engine-dom. Instead of returning a nativeHTMLElement, this method returns aServerHTMLElementwith the public properties, aria reflected properties and HTML global attributed.renderToString(element: ServerHTMLElement): string: This method creates an LWC component tree synchronously and serializes it to HTML. It accepts a single parameter, aServerHTMLElementreturned bycreateElementand returns the serialized string.
This package injects mock DOM APIs in the @lwc/engine-core rendering engine. Those DOM APIs produce a lightweight DOM structure. This structure can then be serialized into a string containing the HTML serialization of the element's descendants. As described in the Appendix, this package is also in charge of attaching on the global object a mock CustomEvent.
- Requires a complete rewrite of the current
@lwc/engine - Might require substantial changes to the existing tools (jest preset, rollup plugins, etc.) to load the right engine depending on the environment.
Prior the evaluation of the LWC engine, we would evaluate a DOM implementation write in JavaScript (like jsdom or domino). The compelling aspect of this approach is that it requires almost no change to the engine to work.
There are multiple drawbacks with this approach:
- Performance: The engine only relies on a limited set of well-known APIs, leveraging a full DOM implementation for SSR would greatly reduce the SSR performance.
- Predictability: By attaching the DOM interfaces on the global object, those APIs are not only exposed to the engine but also to the component author. Exposing such APIs to the component author might bring unexpected behavior when component code runs on the server.
TBD
TBD
How to load the right version of the engine when resolving the lwc identifier?
For example, when running in Node.js lwc might resolve to @lwc/engine-server for SSR purposes, but also to @lwc/engine-dom when running test via jest and jsdom.
One way to solve this would be to import the platform-specific APIs from the appropriate module: createElement from @lwc/engine-dom, createElement and renderToString from @lwc/engine-server and use. Platform-agnostic APIs should be imported from @lwc (which becomes an alias to @lwc/engine-core).
We can break-down the current LWC DOM usages into 3 different categories:
- DOM constructors used by the engine
- DOM methods and accessors used by the engine
- DOM constructors used by component authors
The engine currently relies on the following DOM constructors during evaluation and at runtime:
- DOM usage: Used by the engine to extract the descriptors and reassign them to the
LightningElementprototype. - SSR usage: π΅ NOT REQUIRED
- Observations: We can create a hard-coded list of all the needed accessors: HTML global attributes, aria reflection properties and HTML the good part.
- DOM usage: Used by the engine to traverse the DOM tree to find the error boundary and create the component call stack.
- SSR usage: πΆ MIGHT BE REQUIRED
- Observations: Framework implementing the error boundary concept (like React or Vue), use equivalent of the LWC
VM.ownerback-pointer to traverse the component tree up instead of relying on the DOM tree. This discussion might require another RFC.
On top of this the engine also rely on the following DOM methods and accessors:
- DOM usage: Exposed via
LightningElement.prototype.dispatchEvent() - SSR usage: π΄ REQUIRED
- Observations: Components may dispatch event once connected.
- DOM usage: Exposed via
LightningElement.prototype.addEventListener(). Used by the rendering engine to handleon*directive from the template. - SSR usage: π΄ REQUIRED
- DOM usage: Exposed via
LightningElement.prototype.removeElementListener(). - SSR usage: π΄ REQUIRED
- DOM usage: Used by the upgrade mechanism, synthetic shadow styling and to enforce restrictions.
- SSR usage: π΅ NOT REQUIRED
- Observations: This API can be replaced by
Node.prototype.insertBefore(elm, null).
- DOM usage: Used by the upgrade mechanism, by the rendering engine, and to enforce restrictions.
- SSR Usage: π΄ REQUIRED
- DOM usage: Used by the upgrade mechanism, by the rendering engine, and to enforce restrictions.
- SSR Usage: π΄ REQUIRED
- DOM usage: Used by the upgrade mechanism, and to enforce restrictions
- SSR Usage: π΄ REQUIRED
- DOM usage: Used to traverse the DOM tree
- SSR Usage: πΆ MIGHT BE REQUIRED
- Observations: Depending on how the error boundary is implemented, we might be able to get rid of this API.
- DOM usage: Used by the aria reflection polyfill
- SSR Usage: π΅ NOT REQUIRED
- Observations: For SSR, we will not need this polyfill
- DOM usage: Exposed via
LightningElement.prototype.getAttribute(). Used by the aria reflection polyfill - SSR Usage: πΆ MIGHT BE REQUIRED
- Observations: We might use
Element.prototype.getAttributeNS(null, name)to reduce the API surface needed by the engine.
- DOM usage: Exposed via
LightningElement.prototype.getAttributeNS() - SSR Usage: π΄ REQUIRED
- DOM usage: Exposed via
LightningElement.prototype.setAttribute(). Used by the rendering engine, for synthetic shadow styling and by the aria reflection polyfill. - SSR Usage: πΆ MIGHT BE REQUIRED
- Observations: We might use
Element.prototype.setAttributeNS(null, name)to reduce the API surface needed by the engine.
- DOM usage: Exposed via
LightningElement.prototype.setAttributeNS(). Used by the rendering engine. - SSR Usage: π΄ REQUIRED
- DOM usage: Exposed via
LightningElement.prototype.removeAttribute(). Used by the rendering engine, for the synthetic shadow styling and by the aria reflection polyfill - SSR Usage: πΆ MIGHT BE REQUIRED
- Observations: We might use
Element.prototype.removeAttributeNS(null, name)to reduce the API surface needed by the engine.
- DOM usage: Exposed via
LightningElement.prototype.removeAttributeNS(). - SSR Usage: π΄ REQUIRED
-
DOM usage: Exposed via
LightningElement.prototype.getElementsByTagName(). -
SSR Usage: π΅ NOT REQUIRED
-
Observations: This method is only exposed to select elements from the component light DOM, which is only available from the
renderedCallback. Since SSR doesn't runrenderedCallback, we can always returns an emptyHTMLCollectionwhen running on the server.
- DOM usage: Exposed via
LightningElement.prototype.getElementsByClassName(). - SSR Usage: π΅ NOT REQUIRED
- Observations: This method is only exposed to select elements from the component light DOM, which is only available from the
renderedCallback. Since SSR doesn't runrenderedCallback, we can always returns an emptyHTMLCollectionwhen running on the server.
- DOM usage: Exposed via
LightningElement.prototype.querySelector(). Used to enforce restrictions. - SSR Usage: π΅ NOT REQUIRED
- Observations: This method is only exposed to select elements from the component light DOM, which is only available from the
renderedCallback. Since SSR doesn't runrenderedCallback, we can always returns an emptyNodeListwhen running on the server.
- DOM usage: Exposed via
LightningElement.prototype.querySelectorAll(). Used to enforce restrictions. - SSR Usage: π΅ NOT REQUIRED
- Observations: This method is only exposed to select elements from the component light DOM, which is only available from the
renderedCallback. Since SSR doesn't runrenderedCallback, we can always returns an emptyNodeListwhen running on the server.
- DOM usage: Exposed via
LightningElement.prototype.getBoundingClientRect(). - SSR Usage: π΅ NOT REQUIRED
- Observations: Running a layout engine during SSR, is a complex task that will not bring much values to component authors. Returning an empty
DOMRect(where all the properties are set to0), might be the best approach here.
- DOM usage: Exposed via
LightningElement.prototype.classList. Used by the rendering engine. - SSR Usage: π΄ REQUIRED
- DOM usage: Used to traverse the DOM.
- SSR Usage: πΆ MIGHT BE REQUIRED
- Observations: Depending on how the error boundary is implemented, we might be able to get rid of this API.
- DOM usage: Used to reset the shadow root.
- SSR Usage: π΅ NOT REQUIRED
- Observations: This is a performance optimization to quickly reset the ShadowRoot. We can use
Node.prototype.removeChildas a replacement.
- DOM usage: Exposed on the
LightningElement.prototypeUsed by the rendering to set the properties on custom elements. - SSR Usage: π΄ REQUIRED
Finally, on top of all the APIs used by the engine to evaluate and run, component authors need to have access to the following DOM constructors in the context of SSR.
- DOM usage: Used to dispatch events
- SSR Usage: π΄ REQUIRED
- DOM usage: Used to dispatch events
- SSR Usage: πΆ MIGHT BE REQUIRED
- Observations: This might not be needed because
CustomEventinherits fromEventand becauseCustomEventis the recommended way to dispatch non-standard events.