From 17e7327e19eb07e6f3e18130f7f8283676798224 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 4 Jun 2024 15:55:03 -0700 Subject: [PATCH 1/9] RFC: Native lifecycle mixed mode --- text/0000-native-lifecycle-mixed-mode.md | 152 +++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 text/0000-native-lifecycle-mixed-mode.md diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md new file mode 100644 index 00000000..953dca11 --- /dev/null +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -0,0 +1,152 @@ +--- +title: Native lifecycle mixed mode +status: DRAFTED +created_at: 2024-06-04 +updated_at: 2024-06-04 +pr: https://github.com/salesforce/lwc-rfcs/pull/89 +--- + +# Native lifecycle mixed mode + +## Summary + +LWC ships with a polyfill for two [custom element lifecycle hooks](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components/Using_custom_elements#custom_element_lifecycle_callbacks): `connectedCallback` and `disconnectedCallback`. This polyfill was originally designed for IE11, but has persisted due to backwards compatibility concerns. + +The polyfill (otherwise known as "synthetic lifecycle") also affects component rendering and `renderedCallback`, since the LWC engine only does an initial render when `connectedCallback` is invoked. + +An attempt was made during the LWC v6 release (Salesforce Summer '24) to [upgrade components to native lifecycle based on API versioning](https://github.com/salesforce/lwc/releases/v6.0.0#native-lifecycle). This turned out to be non-viable, so it was rolled back. + +The reason it was non-viable is that mixing components running in two modes (synthetic vs native lifecycle) causes observable changes even for components that were not expecting their lifecycle callback timings to change, due to changes in their slot container. (An example of this is spelled out in detail [here](https://github.com/salesforce/lwc/issues/4249).) + +To resolve this, this RFC proposes a new "native lifecycle mixed mode," inspired by [mixed shadow DOM mode](https://github.com/salesforce/lwc-rfcs/blob/master/text/0115-mixed-shadow-mode.md). It would work in much the same way, allowing components to gradually opt-in to native custom element lifecycle, while also maintaining consistency within the descendant DOM tree for that component. + +## Basic example + +Opting in: + +```js +export default class extends LightningElement { + static lifecycleSupportMode = 'native'; +} +``` + +Similar to shadow DOM mixed mode, you can also opt out: + +```js +export default class extends LightningElement { + static lifecycleSupportMode = 'reset'; +} +``` + +Like shadow DOM mixed mode, this `'reset'` is mostly useful if you have a superclass that declares `'native'`, which you would like to override. + +## Motivation + +The synthetic lifecycle polyfill is technical debt. If LWC were designed from scratch today, it wouldn't ship with its own implementation of `connectedCallback` or `disconnectedCallback` – it would just defer to the native browser behavior. + +Unfortunately, due to subtle differences between native and synthetic lifecycle, and due to the Lightning Platform's backwards compatibility guarantees, it is not trivial to swap one implementation out for the other. + +At the same time, component authors often complain about bugs in synthetic lifecycle ([and](https://github.com/salesforce/lwc/issues/2609) [there](https://github.com/salesforce/lwc/issues/3361) [are](https://github.com/salesforce/lwc/issues/1452) [many](https://github.com/salesforce/lwc/issues/3823)). The most common complaint is that `disconnectedCallback` does not consistently fire, which can lead to memory leaks or buggy behavior. Unfortunately, fixing these bugs would necessarily create observable behavior that would break some other component that relies on the bug (see [Hyrum's Law](https://www.hyrumslaw.com/)). + +To unblock component authors who _do_ want non-buggy behavior for `connectedCallback`/`disconnectedCallback`, we should allow them to opt-in to native lifecycle. This paves the way for a polyfill-free future, while still preserving backwards compatibility and giving a migration path for authors who need time to upgrade. + +## Detailed design + +### The `lifecycleSupportMode` property + +Like `shadowSupportMode`, `lifecycleSupportMode` has two possible values: + +- `'native'` +- `'reset'` (default, i.e. synthetic mode) + +Any other value throws an error at runtime. + +### Polyfill behavior + +Similar to [`@lwc/synthetic-shadow`](https://www.npmjs.com/package/@lwc/synthetic-shadow) and [`@lwc/aria-reflection`](https://www.npmjs.com/package/@lwc/aria-reflection), we will isolate the code necessary for synthetic lifecycle into a standalone package: `@lwc/synthetic-lifecycle`. The polyfill can be loaded using simply: + +```js +import '@lwc/synthetic-lifecycle'; +``` + +This allows `@lwc/engine-dom` to run in two modes: + +1. **Purely native**. This is the case where the polyfill is not loaded. All components run with native lifecycle. +2. **Mixed mode**. This is the case where the polyfill _is_ loaded. This is the only case where `lifecycleSupportMode` actually does anything. Components that specify `static lifecycleSupportMode = 'native'` will opt-out of the polyfill. + +This has a few advantages: + +1. `@lwc/engine-dom` is "pure" and polyfill-free by default, which gives off-core consumers the best possible LWC experience. +2. It aligns neatly with the existing behavior for LWC's other polyfills. +3. If consumers do not need the polyfill, then they do not need to load the code for the polyfill (which admittedly is small – roughly 385 bytes minified+gzipped). +4. It does not enshrine `lwcRuntimeFlags` as a user-facing API surface, which is the current ad-hoc solution to toggling native vs synthetic lifecycle. + +### Transitivity + +Like shadow DOM mixed mode, lifecycle mixed mode will need some concept of transitivity. In particular, it will enforce the following rules: + +1. Any descendants of a native-lifecycle component are forced into native lifecycle mode. (Same as shadow DOM mixed mode.) +2. Any _slotted content_ of a native-lifecycle component are also forced into native lifecycle mode. (Different from shadow DOM mixed mode.) + +The reason for rule #2 is that slots in particular proved to be a source of observable changes for native-lifecycle components. It is simply the case that a slotted component can observe whether the component it is being slotted into is native or synthetic. + +The reason for rule #1 is that, exactly like slotted content, children can observe the mode of their parents. + +So rather than trying to emulate synthetic-lifecycle-within-native-lifecycle (which is [a fool's errand](https://github.com/salesforce/lwc/issues/4249)), we should force the entire tree into native lifecycle mode so that `connectedCallback` and `disconnectedCallback` at least consistently fire. + +For component authors, this means that they will have to be careful when enabling native lifecycle mode. Similar to native shadow mode, they will have to ensure that their children are all native-shadow ready. But in addition, they must communicate to their consumers that any _slotted_ content that consumers provide is also beholden to this rule. + +For components with a longstanding history and wide variety of consumers, this might not be feasible. Component authors will have to decide for themselves whether they are ready to ship their components in native lifecycle or not. + +> [!NOTE] +> There is no scenario where LWC will "move" a component from one component's slot to another component's slot. Instead, it will unmount (destroy) and mount (create) a new component in this case. This means that LWC does not have to re-evaluate a component's lifecycle mode after the initial mount. However, if a component is manually removed/inserted, then all bets are off. (Component authors typically do not do this.) + +### Testing + +For Jest testing, we can follow the same model as the existing [`nativeShadow`](https://github.com/salesforce/lwc-test/blob/master/packages/%40lwc/jest-preset/README.md#nativeshadow) config option: + +```json +{ + "globals": { + "lwc-jest": { + "nativeLifecycle": true + } + } +} +``` + +In other words, Jest tests will assume synthetic lifecycle as the default, but will allow opting-in to lifecycle mixed mode. + +> [!NOTE] +> The decision of which mode is used by default can be debated later. After all, Jest is not production – it is just for informational purposes. But for now, it makes sense to align with the existing `nativeShadow` option. + +## Drawbacks + +The main drawbacks of this proposal are: + +- It creates two new confusing modes for LWC to run in (with a confusing name as well – although "synthetic lifecycle" seems to have stuck). +- It adds complexity without actually removing global polyfills from LWC (which will still be required as long as any component on the page uses synthetic lifecycle). +- It will add some complexity to the code to extract out the polyfill to a separate package, and to create a handshake between that package and `@lwc/engine-dom`. +- Component authors may not migrate as fast as we'd like them to, since it is opt-in and provides abstract benefits compared to a very real risk of breakage. + +## Alternatives + +Obviously API versioning was considered, but rolled back for reasons spelled out above. + +Another alternative is to just _never_ ship native lifecycle, but due to the widespread bugs in the polyfill and volume of user complaints about it, this alternative does not seem reasonable. + +## Adoption strategy + +We can certainly write [a codemod](https://github.com/salesforce/lwc-codemod) for this – it is trivial to add a static property. Unfortunately, the codemod cannot detect or fix any bugs caused by the migration from synthetic to native lifecycle. + +On the other hand, we do already have runtime warnings that detect when synthetic lifecycle is executing `connectedCallback` on a disconnected DOM tree, and we can use this warning message as an opportunity to point component authors to native lifecycle. + +The adoption strategy for native lifecycle can also be folded into the strategy for native shadow DOM, since the shape of the solution largely rhymes. + +# How we teach this + +The names "synthetic" and "native" here were deliberately chosen to resemble the [existing documentation around native shadow DOM](https://developer.salesforce.com/blogs/2024/01/get-your-lwc-components-ready-native-shadow-dom). Teaching one should give us a perfect opportunity to teach about the other. + +# Unresolved questions + +None at this time. From 33f79f7cb30432b007ba58eb524118dc4762c995 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 4 Jun 2024 15:59:18 -0700 Subject: [PATCH 2/9] fix: typo --- text/0000-native-lifecycle-mixed-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md index 953dca11..56759e98 100644 --- a/text/0000-native-lifecycle-mixed-mode.md +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -86,7 +86,7 @@ This has a few advantages: Like shadow DOM mixed mode, lifecycle mixed mode will need some concept of transitivity. In particular, it will enforce the following rules: 1. Any descendants of a native-lifecycle component are forced into native lifecycle mode. (Same as shadow DOM mixed mode.) -2. Any _slotted content_ of a native-lifecycle component are also forced into native lifecycle mode. (Different from shadow DOM mixed mode.) +2. Any _slotted content_ of a native-lifecycle component is also forced into native lifecycle mode. (Different from shadow DOM mixed mode.) The reason for rule #2 is that slots in particular proved to be a source of observable changes for native-lifecycle components. It is simply the case that a slotted component can observe whether the component it is being slotted into is native or synthetic. From 55baff60706f8f2f1447e7024b1d126e20f9d324 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 4 Jun 2024 16:00:41 -0700 Subject: [PATCH 3/9] fix: clarify --- text/0000-native-lifecycle-mixed-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md index 56759e98..a4ecc21b 100644 --- a/text/0000-native-lifecycle-mixed-mode.md +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -88,7 +88,7 @@ Like shadow DOM mixed mode, lifecycle mixed mode will need some concept of trans 1. Any descendants of a native-lifecycle component are forced into native lifecycle mode. (Same as shadow DOM mixed mode.) 2. Any _slotted content_ of a native-lifecycle component is also forced into native lifecycle mode. (Different from shadow DOM mixed mode.) -The reason for rule #2 is that slots in particular proved to be a source of observable changes for native-lifecycle components. It is simply the case that a slotted component can observe whether the component it is being slotted into is native or synthetic. +The reason for rule #2 is that slots in particular proved to be a source of observable changes for native-lifecycle components. It is simply the case that a slotted component can observe the timing behavior of the component it is being slotted into. The reason for rule #1 is that, exactly like slotted content, children can observe the mode of their parents. From fe8ed69ec4f33bfd7c63bb6d83ab914469f34c22 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 4 Jun 2024 16:01:49 -0700 Subject: [PATCH 4/9] fix: clarify --- text/0000-native-lifecycle-mixed-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md index a4ecc21b..e59c9bdf 100644 --- a/text/0000-native-lifecycle-mixed-mode.md +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -88,7 +88,7 @@ Like shadow DOM mixed mode, lifecycle mixed mode will need some concept of trans 1. Any descendants of a native-lifecycle component are forced into native lifecycle mode. (Same as shadow DOM mixed mode.) 2. Any _slotted content_ of a native-lifecycle component is also forced into native lifecycle mode. (Different from shadow DOM mixed mode.) -The reason for rule #2 is that slots in particular proved to be a source of observable changes for native-lifecycle components. It is simply the case that a slotted component can observe the timing behavior of the component it is being slotted into. +The reason for rule #2 is that slots in particular proved to be a source of observable changes for native-lifecycle components. It is simply the case that a slotted component can observe the lifecycle mode of the component it is being slotted into, due to connected/disconnect events triggering downstream callbacks in the tree. The reason for rule #1 is that, exactly like slotted content, children can observe the mode of their parents. From 127ceaca0bf677c953c521557a805e0184289bcc Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 4 Jun 2024 16:02:15 -0700 Subject: [PATCH 5/9] fix: typo --- text/0000-native-lifecycle-mixed-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md index e59c9bdf..c01bce72 100644 --- a/text/0000-native-lifecycle-mixed-mode.md +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -88,7 +88,7 @@ Like shadow DOM mixed mode, lifecycle mixed mode will need some concept of trans 1. Any descendants of a native-lifecycle component are forced into native lifecycle mode. (Same as shadow DOM mixed mode.) 2. Any _slotted content_ of a native-lifecycle component is also forced into native lifecycle mode. (Different from shadow DOM mixed mode.) -The reason for rule #2 is that slots in particular proved to be a source of observable changes for native-lifecycle components. It is simply the case that a slotted component can observe the lifecycle mode of the component it is being slotted into, due to connected/disconnect events triggering downstream callbacks in the tree. +The reason for rule #2 is that slots in particular proved to be a source of observable changes for native-lifecycle components. It is simply the case that a slotted component can observe the lifecycle mode of the component it is being slotted into, due to connect/disconnect events triggering downstream callbacks in the tree. The reason for rule #1 is that, exactly like slotted content, children can observe the mode of their parents. From 4b372a5f78cdfbfeb405806dd2afa2fa9c9a9531 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 4 Jun 2024 16:02:56 -0700 Subject: [PATCH 6/9] fix: typo --- text/0000-native-lifecycle-mixed-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md index c01bce72..f1d5750b 100644 --- a/text/0000-native-lifecycle-mixed-mode.md +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -94,7 +94,7 @@ The reason for rule #1 is that, exactly like slotted content, children can obser So rather than trying to emulate synthetic-lifecycle-within-native-lifecycle (which is [a fool's errand](https://github.com/salesforce/lwc/issues/4249)), we should force the entire tree into native lifecycle mode so that `connectedCallback` and `disconnectedCallback` at least consistently fire. -For component authors, this means that they will have to be careful when enabling native lifecycle mode. Similar to native shadow mode, they will have to ensure that their children are all native-shadow ready. But in addition, they must communicate to their consumers that any _slotted_ content that consumers provide is also beholden to this rule. +For component authors, this means that they will have to be careful when enabling native lifecycle mode. Similar to native shadow mode, they will have to ensure that their children are all native-lifecycle ready. But in addition, they must communicate to their consumers that any _slotted_ content that consumers provide is also beholden to this rule. For components with a longstanding history and wide variety of consumers, this might not be feasible. Component authors will have to decide for themselves whether they are ready to ship their components in native lifecycle or not. From 510956e760fc23313aa4e2c7249182c832afc84d Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 4 Jun 2024 16:03:30 -0700 Subject: [PATCH 7/9] fix: wording --- text/0000-native-lifecycle-mixed-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md index f1d5750b..f96563ea 100644 --- a/text/0000-native-lifecycle-mixed-mode.md +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -94,7 +94,7 @@ The reason for rule #1 is that, exactly like slotted content, children can obser So rather than trying to emulate synthetic-lifecycle-within-native-lifecycle (which is [a fool's errand](https://github.com/salesforce/lwc/issues/4249)), we should force the entire tree into native lifecycle mode so that `connectedCallback` and `disconnectedCallback` at least consistently fire. -For component authors, this means that they will have to be careful when enabling native lifecycle mode. Similar to native shadow mode, they will have to ensure that their children are all native-lifecycle ready. But in addition, they must communicate to their consumers that any _slotted_ content that consumers provide is also beholden to this rule. +For component authors, this means that they will have to be careful when enabling native lifecycle mode. Similar to native shadow mode, they will have to ensure that their children are all native-lifecycle ready. But in addition, they must communicate to their consumers that any _slotted_ content provided by the consumer is also beholden to this rule. For components with a longstanding history and wide variety of consumers, this might not be feasible. Component authors will have to decide for themselves whether they are ready to ship their components in native lifecycle or not. From 893a5125012c817507663754c23932e925ffe78c Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 4 Jun 2024 16:10:20 -0700 Subject: [PATCH 8/9] fix: wording --- text/0000-native-lifecycle-mixed-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md index f96563ea..0aeb9a15 100644 --- a/text/0000-native-lifecycle-mixed-mode.md +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -18,7 +18,7 @@ An attempt was made during the LWC v6 release (Salesforce Summer '24) to [upgrad The reason it was non-viable is that mixing components running in two modes (synthetic vs native lifecycle) causes observable changes even for components that were not expecting their lifecycle callback timings to change, due to changes in their slot container. (An example of this is spelled out in detail [here](https://github.com/salesforce/lwc/issues/4249).) -To resolve this, this RFC proposes a new "native lifecycle mixed mode," inspired by [mixed shadow DOM mode](https://github.com/salesforce/lwc-rfcs/blob/master/text/0115-mixed-shadow-mode.md). It would work in much the same way, allowing components to gradually opt-in to native custom element lifecycle, while also maintaining consistency within the descendant DOM tree for that component. +To resolve this, this RFC proposes a new "native lifecycle mixed mode," inspired by [mixed shadow DOM mode](https://github.com/salesforce/lwc-rfcs/blob/master/text/0115-mixed-shadow-mode.md). It would work in much the same way, allowing components to gradually opt-in to native custom element lifecycle, while also maintaining consistency within the DOM tree for that component. ## Basic example From 6b9df08536a813838879d7cf84929f45d25fbc26 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 11 Jun 2024 12:08:34 -0700 Subject: [PATCH 9/9] fix: question --- text/0000-native-lifecycle-mixed-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-native-lifecycle-mixed-mode.md b/text/0000-native-lifecycle-mixed-mode.md index 0aeb9a15..39eb1ddc 100644 --- a/text/0000-native-lifecycle-mixed-mode.md +++ b/text/0000-native-lifecycle-mixed-mode.md @@ -149,4 +149,4 @@ The names "synthetic" and "native" here were deliberately chosen to resemble the # Unresolved questions -None at this time. +- Do we _need_ to have the additional transitivity for slotted content? It would be simpler to use the same model as shadow DOM mixed mode. Maybe we can just fix [the main bugs](https://github.com/salesforce/lwc/pull/4248) when slotting synthetic-lifecycle components into native-lifecycle components, and accept that there will be some minor differences ([as with shadow DOM mixed mode](https://github.com/salesforce/lwc/issues/4267))?