-
Notifications
You must be signed in to change notification settings - Fork 25
RFC: LWC server runtime proposal #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
text/0112-server-runtime.md
Outdated
- `createElement(name: string, options: { is: typeof LightningElement }): ServerHTMLElement`: This method creates a new LWC component tree. It follows the same signature as the `createElement` API from `@lwc/engine-dom`. Instead of returning a native `HTMLElement`, this method returns a `ServerHTMLElement` with the public properties, aria reflected properties and HTML global attributed. | ||
- `renderToString(element: ServerHTMLElement): string`: This method creates an LWC component tree synchronously and serialize it to string. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. | ||
|
||
This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produces a lightweight DOM structure that can be serialized into a string by the `renderToString` method. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produces a lightweight DOM structure that can be serialized into a string by the `renderToString` method. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent`. | |
This package injects mock DOM APIs in the `@lwc/engine-core` rendering engine. Those DOM APIs produce a lightweight DOM structure that can be serialized into a string by the `renderToString` method. As described in the Appendix, this package is also in charge of attaching on the global object a mock `CustomEvent`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those DOM APIs produce a lightweight DOM structure
Which DOM implementation do you plan to use for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it will be our own bake implementation... We do not plan to allow user-code to talk to the dom, and if they do, we will throw or ignore. The mocks needed here (btw, I will not call them mocks, but implementation) are those APIs needed for the engine to function, so it goes hand to hand.
text/0112-server-runtime.md
Outdated
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also add scalability. The mocked APIs would have to be kept in sync with what's actually being used in the engine. Also the engine still wouldn't be reusable in other environments without a corresponding "mock" layer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it is the case. There is only a well-defined set of DOM APIs the engine requires to operate. Each runtime (dom and server) is in charge of implementing this contract by passing an implementation of those APIs to the engine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it has stabilized by now but when I took that approach I had to add/change multiple mocked APIs during the couple months I touched that code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that will be that challenging, we will own both packages, and we can keep them in sync.
text/0112-server-runtime.md
Outdated
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-core` exposes platform-agnostic APIs and will be used by the different runtime packages to share the common logic. | ||
- `@lwc/engine-dom` exposes LWC APIs available on the browser. | ||
- `@lwc/engine-server` exposes LWC APIs used for server-side rendering. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a fan of the name. engine-dom
describes the LWC execution context and doesn't imply a client or server environment. engine-server
describes one possible environment for a dom-less execution context. We can bike shed on naming, but I'd prefer engine-string
or something similar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I wasn't happy with the naming.
What do you think about engine-dom-renderer
and engine-string-renderer
? Even if the package names are more verbose I don't think it's a problem because the only thing developers have to remember if lwc
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about @lwc/engine-core
, @lwc/engine
, and @lwc/engine-string
? This would minimize the impact of renaming existing packages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also like the inverted version: @lwc/client-engine
, @lwc/server-engine
, @lwc/engine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there will be a renaming impact here. The @lwc/engine
module should never be referenced directly. Userland code should only reference lwc
, the only place where we might need to change is on the resolver.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm with @ekashida on this, this is NOT about client and server I'm afraid, this is about dom vs dom string. Imagine that in the future we decide to use the dom string output in a client for some reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[...] this is about dom vs dom string.
I fully agree with this statement. In this regard, I think it would less confusing if the module that is loaded in a DOM environment contains the term dom
in its name. I would suggest reaming @lwc/engine
to @lwc/engine-dom
in @ekashida proposal.
text/0112-server-runtime.md
Outdated
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 the `createElement` API from `@lwc/engine-dom`. Instead of returning a native `HTMLElement`, this method returns a `ServerHTMLElement` with the public properties, aria reflected properties and HTML global attributed. | ||
- `renderToString(element: ServerHTMLElement): string`: This method creates an LWC component tree synchronously and serialize it to string. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing worth exploring: Can we pass @api
data for the top level component here? If values are known at "tree creation time", we can probably hydrate a good portion of the LWC tree sync as the initial tree is being created
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is probably another area to explore in a different RFC since it touches on how much can you render on the server synchronously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the data is known before the component is rendered, the current proposal should be sufficient.
import { createElement, renderToString } from "lwc";
import App from "c/app";
const app = createElement("c-app", { is: App });
app.foo = 'Some value';
app.bar = ['Some', 'other', 'value.'];
const str = renderToString(app);
console.log(str);
Like a standard HTMLElement
returned by createElement
when LWC is running in the browser, the ServerHTMLElement
return by created
createElement` on the server would expose:
- the component public properties
- the global HTML properties
- aria reflection properties
text/0112-server-runtime.md
Outdated
|
||
TBD | ||
|
||
## How we teach this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would probably be a major version change requiring new documentation.
text/0112-server-runtime.md
Outdated
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-core` exposes platform-agnostic APIs and will be used by the different runtime packages to share the common logic. | ||
- `@lwc/engine-dom` exposes LWC APIs available on the browser. | ||
- `@lwc/engine-server` exposes LWC APIs used for server-side rendering. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about @lwc/engine-core
, @lwc/engine
, and @lwc/engine-string
? This would minimize the impact of renaming existing packages.
Co-Authored-By: Eugene Kashida <[email protected]>
text/0112-server-runtime.md
Outdated
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 the `createElement` API from `@lwc/engine-dom`. Instead of returning a native `HTMLElement`, this method returns a `ServerHTMLElement` with the public properties, aria reflected properties and HTML global attributed. | ||
- `renderToString(element: ServerHTMLElement): string`: This method synchronously serializes an LWC component tree to HTML. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and returns the serialized string. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How are the components stylesheets passed as part of the string? Are they pre-pended to the string as a <style> tag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one area that will require more investigation. The serialization format is outside the scope of this proposal and will be subject to another RFC: https://github.com/salesforce/lwc-rfcs/blob/ccbd9f38a7ff30d2bd604107e6eb13608366aed2/text/0112-server-runtime.md#scope
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seckardt Maybe you want to share your investigation in this area?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On our first baby steps towards SSR I didn't reinvent the wheel in that area. I simply reused existing functionality as provided by the StylesheetFactory
.
text/0112-server-runtime.md
Outdated
- `createElement(name: string, options: { is: typeof LightningElement }): ServerHTMLElement`: This method creates a new LWC component tree. It follows the same signature as the `createElement` API from `@lwc/engine-dom`. Instead of returning a native `HTMLElement`, this method returns a `ServerHTMLElement` with the public properties, aria reflected properties and HTML global attributed. | ||
- `renderToString(element: ServerHTMLElement): string`: This method synchronously serializes an LWC component tree to HTML. It accepts a single parameter, a `ServerHTMLElement` returned by `createElement` and 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`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The POC directly serializes the VDOM, without the need of a DOM structure. Why is a lightweight DOM necessary? Isn't that an extra generation step that would impact the performance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this DOM structure will have a performance impact however there needs to be a DOM structure (even really lightweight) to attach the event listeners and dispatch the DOM events.
text/0112-server-runtime.md
Outdated
pr: https://github.com/salesforce/lwc-rfcs/pull/23 | ||
--- | ||
|
||
# LWC server runtime |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pmdartus "LWC Server Runtime" sounds like an application framework for running LWC, like LWR (and runtime is super overloaded). Can we rename this to "LWC Server Engine" or "LWC DOM Decoupling"?
Thanks @jasonsilberman! Co-authored-by: Jason Silberman <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm hoping someone will take a look at the complexity of implementing a one time snapshot of wired data during SSR. If SSR comes out with wire adapters disabled it will significantly decrease its appeal.
At least expose an option? Or a plugin mechanism to enable it to work?
|
||
**The `renderedCallback` lifecycle hook will not execute on the server:** When running in a browser, this hook is the first life cycle hook which gives the component author access to the rendered DOM elements. If the component were to attempt to access those APIs on the server it would result in a runtime error since the DOM APIs are not mocked. | ||
|
||
**Wire adapters will not be invoked:** The Wire protocol emits a stream of data to a component. The current protocol doesn't define a way today to indicate that the stream is done emitting new data. Because of this, the first version of SSR will not invoke any wire adapter. The protocol will need to be changed and new primitives will need to be added to LWC to make wire adapters compatible with SSR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabling wire adapters during SSR eliminates the ability to use wire protocol to inject initial state including localized text in the SSR output. Can't we invoke the wire's constructor, connect and disconnect and capture a one time snapshot init data of the wire?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of today, the invocation timing of the update
callback is different if the wire adapter has dynamic parameters or not. This difference makes it impossible to invoke wire adapters in a reliable fashion.
Changing the current invocation timing is an observable change that might result in breaking existing customers on the platform. We decided to set wire adapters aside from the initial SSR implementation but it is something that we will reconsider in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this can enabled as an optional configuration param when we call renderLwcToString
or something like that. By default wire adapters can be skipped, but users can opt-in if their use case is deterministic like in my case, I don't connect wire adapters to network requests, they are simply connected to redux style stores to decouple state and UI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a reasonable compromise would be to support Wire adapters without dynamic parameters during SSR, which would cover several use-cases (including my own).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, we should make wire execution predictable (both on the client and on the server). It would be surprising that for a developer to discover that a wire stops getting executed in SSR after adding a dynamic param.
Over the last 6 months, the LWC team has been investigating how we could roll out potentially risky changes like this one (changing the invocation timing of wires) without breaking existing customers in production. I hope that we could leverage one of the approaches we are discussing to solve this issue and enable wire adapter execution for SSR.
/cc @nolanlawson
@pmdartus I also believe that with a fetch method, lots of use cases for the wired methods would no longer exist. How would ssr work on a lightning page? Would salesforce let us control which lwcs are rendered on the server, or would it be all or none? export default class comp extends LightningComponents {
fetch(context){
/* context
user => exposes current session permissions
query => runs queries soql/sosl
isBrowser => this could also be available on the middleware. This way we can interact with data stores in the middleware if available
inside this scope, the server could let us use "some" of node functions/modules, such as the native fetch???? With the fetchrm function I would be able to get data from other authorized services (maybe controlled by remote site settings entries).
I could also use the `query` object from `context` to fetch data from Salesforce in a secure way.
*/
}
} |
Middlewares that can run on both client/server could also be useful to control the initial state of a component depending on the User permissions. If ssr is enabled on salesforce, the context object could expose objects to interact with Salesforce APIs export default class comp extends LightningElement{
middleware(context){
/*context has objects such as
user => exposes current session permissions
query => runs queries soql/sosl
isBrowser => this could also be available on the middleware. This way we can interact with data stores in the middleware if available
*/
}
Or
this.middleware = commonMiddleware //must be imported as another module. It is useful for sharing middlewares to many components
} |
@AllanOricil the fetch method is an interesting approach, NextJS offers something called getServerSideProps that does something similar. My issue with this proposal, or any other proposal that provides an alternative to wire adapters is that it basically requires developers who leverage wire adapters for their data fetching to re-engineer their components to be server side rendered. This may not be avoidable due to some foundational architecture decision, but it's a real shame. The wire adapter API is one of the best things about LWC, forking the ecosystem by saying on clients wire adapters are supported, but on servers they are not, is going to be a serious barrier for adopting server side rendering. Perhaps a reasonable compromise would be to support Wire adapters without dynamic parameters during SSR, which would cover several use-cases (including my own). |
@gamedevsam More about nuxt fetch
If you think a |
We should maybe take this discussion elsewhere (maybe Slack? Hit me up
Wire adapters can result in an invocation of a wire method, as well as wire values directly into member variables. If a wire adapter invokes a method on a component, that method often assigns values into member variables of the component. These variables can (and often are) accessed in the template of the component. If we render the template without invoking wire adapters, the HTML will be rendered using default values provided for those member variables. This may be acceptable for some use cases, but in my case it will break the template rendering, since we assume wire adapters will be invoked before the initial render, and will provide the initial values for member variables. Here's an example of code that would break in several of our repos, where we leverage wire adapters to inject localized text, ex: export default class I18nComponentExample {
// this:
@wire(WireI18n) i18n;
// is equivalent to this:
i18n;
@wire(WireI18n)
i18nConnected(i18n) {
this.i18n = i18n;
}
} Rendering the template without the wire adapter will break this component: <p>{i18n.keyToLocalizedText}</p>
I use wire methods / wire adapter API outside of Salesforce, we use it to wire data from Redux-like data stores in applications hosted on Heroku. Wire adapters are frequently used to pipe networked data into components, and that is the main use case for components rendered within Salesforce. But that isn't their only use case. AppExchange uses them to provide reactivity / connect them to external stores that manage our state. Wire adapters for me are like React hooks, they are in my opinion one of the best features of LWC OSS. |
@gamedevsam my bad https://lwc.dev/guide/wire_adapter#wire-adapters After reading this doc, now I understand how it actually works. |
There are a lot of interesting points that have been raised on this thread. Let me try to answer some of them.
For the record, there is currently no plan to use SSR in the context of Lightning Experience. Salesforce is considering using SSR for other experiences where initial page load time and SEO are important. As a side note, the portability of existing LWC components on the Salesforce platform from client-side rendering only to client + server-side rendering is also something that we discussed at length. While we understand that customers have put a lot of effort into building LWC components on top of the platform, it's important to acknowledge that building client-side-only UI components is vastly different than build isomorphic components. And this, regardless of the UI framework. We are in the middle of porting some of our existing internal LWC components to be SSR compatible, and as part of this journey, some components had to be redesigned from the ground up. Documentation and linting rules are in preparation to help with this migration.
Yes, it was top of mind as we designed SSR for LWC. We are currently investigating an approach similar to Next We also discussed exposing the capability to retrieve async data in arbitrary components (like /cc @nolanlawson @divmain |
Server-side Rendering (SSR) is a popular technique for rendering pages and components that would usually run in a browser on the server. SSR takes as input a root component and a set of properties and renders an HTML fragment and optionally one or multiple stylesheets. This technique can be used in many different ways: improve initial page render time, generate static websites, …
Due to the size and complexity of this feature, the rollout of SSR is broken up in 3 different phases and is spread over multiple releases:
As part of phase 1, the goal is to provide a capability for LWC to render components transparently on the server. From the component standpoint, the LWC SSR and DOM versions provide identical APIs and will run the component in the same fashion on both platforms.
Rendered