-
Notifications
You must be signed in to change notification settings - Fork 423
refactor(engine): add support for template hot swapping #1860
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
Conversation
| // in dev-mode, we support hot swapping of templates, which means that | ||
| // the component instance might be attempting to use an old version of | ||
| // the template, while internally, we have a replacement for it. | ||
| html = getTemplateOrSwappedTemplate(html); |
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 returns the same template provided, or the new version of it based on calls to registerTemplateSwap
| // that is not provided by the component instance. | ||
| validateFields(vm, html); | ||
| // add the VM to the list of host VMs that can be re-rendered if html is swapped | ||
| addHotVM(vm); |
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.
after the vm and the new template were associated, the VM is added to the a set of hot VMs linked to the template, in case the template gets swapped later on.
| // old vnode.children is removed from the DOM. | ||
| export function removeVM(vm: VM) { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| removeHotVM(vm); |
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 to avoid leaking VM instances for components that were removed, but their template still available for new instances. this just complements addHotVM
|
|
||
| export { setFeatureFlag, setFeatureFlagForTest } from '@lwc/features'; | ||
|
|
||
| export { registerTemplateSwap } from './hot-swaps'; |
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.
@diervo we need to restrict this in platform, and this is the one we call with the old template and the new template functions from outside.
packages/integration-karma/test/swapping/templates/index.spec.js
Outdated
Show resolved
Hide resolved
| @@ -0,0 +1,36 @@ | |||
| import { createElement } from '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.
Let's also add tests:
- for components
- for stylesheets swapping for both synthetic shadow and native shadow.
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.
in the making
| // created from a template, root elements can't be swapped because we | ||
| // don't have a way to force the creation of the element with the same state | ||
| // of the current element. | ||
| if (!isNull(owner) && isFalse(owner.isDirty)) { |
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.
Vue 3 automatically reloads the page if the root element is modified: https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/hmr.ts#L108-L118.
I think it makes sense to do that automatically or to add a flag reloadIfNeeded or something like that to registerComponentSwap to reload the page if the component can't be swapped.
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.
@caridy said root might be tricky, I think hot swapping the root doesn't look like a terrible corner case to have.
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.
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.
Another approach would be to notify the caller that the swap can't be done and it's to the caller to decide what to do: display a banner on the page with a reload button, log a warning or an error, ...
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 more complicated, because it is not a boolean operation:
a) the swap was possible (on at least 1 instance)
b) the swap was not possible in X active instances
c) the swap is not needed (no associated instance)
We could make it true/false if we say:
- true is problem was not encounter, which is a and c
- false if at least one instance was root (which mean we were not able to swap it)
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.
ok, figured a way for this that works fine... the apis are going to return a boolean, whether or not the operation succeed or not..., in the case of the template and style they will (for now) always return true. And for components, it will return false if a root instance is changing, so @diervo code can refresh the entire page
|
@diervo Is it possible to add a small snippet in this PR explaining how this API is planned to be used with other tools. I don't think we need to fully document it, but having a small snippet explaining how this is intended to work would be great. |
|
@pmdartus done |
|
I have been looking at how Webpack and parcel are supporting HMR, and I don't have found yet a way to keep a reference to the old module, which makes it impossible to invoke Let's make sure that those new APIs are compatible with Webpack to leverage them in lwc-create-app. /cc @muenzpraeger |
Co-authored-by: Pierre-Marie Dartus <p.dartus@salesforce.com>
🥳 Performance ImprovementBest has detected that there is a Please click here to see more details. Click to view significantly changed benchmarkslwc-engine-benchmark
|
| import { Template } from './template'; | ||
| import { StylesheetFactory } from './stylesheet'; | ||
|
|
||
| const swappedTemplateMap = new WeakMap<Template, Template>(); |
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.
Going with Diego's concept of "everything is a module", we could potentially use 1 WeakMap of modules instead of 3 WeakMaps for the different modules types.
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.
right, but because the reaction mechanism depends on the type that you're swapping, we have 3 of them.
…into caridy/template-hot-swap
🥳 Performance ImprovementBest has detected that there is a Please click here to see more details. Click to view significantly changed benchmarkslwc-engine-benchmark
|
| // the styles will be reset, along with the content of the | ||
| // shadow, this way we can guarantee that the styles will be | ||
| // recalculated, and applied. | ||
| vm.cmpTemplate = () => []; |
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.
We are leaking internal details about how the engine decides to re-render or not a component here (eg. resetting the cmpTemplate). What do you think about exposing a method out of the VM.ts like forceHydrate to scope 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.
I'm fine with that too... will do
| } | ||
| } | ||
|
|
||
| export function swapTemplate(oldTpl: Template, newTpl: Template): boolean { |
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.
@diervo @pmdartus @ravijayaramappa do you think we should check the arguments to be a valid register template?
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, no. It will be checked before evaluation in evaluateTemplate.
!isTemplateRegistered(html)
That check is lazy, the template is validated before it is used. So if you register a template and the engine never uses it, the check is avoided.
| ): boolean { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| swappedComponentMap.set(oldComponent, newComponent); | ||
| return rehydrateHotComponent(oldComponent); |
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.
@diervo @pmdartus @ravijayaramappa do you think we should check the arguments to be a valid registered constructor?
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.
Same here, getComponentInternalDef() takes care of the validation.
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.
@ravijayaramappa the problem here is that this will happen in the next tick, not when you call for the swap
|
|
||
| export function swapStyle(oldStyle: StylesheetFactory, newStyle: StylesheetFactory): boolean { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| swappedStyleMap.set(oldStyle, newStyle); |
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 we have a way to validate whether a style is a compiled style or not... probably we should eventually do some registration from compiler to make sure that people don't mess around with this.
| const list = activeComponents.get(Ctor); | ||
| let canRefreshAllInstances = true; | ||
| list?.forEach((vm) => { | ||
| const { owner } = vm; |
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.
another point to clear up about this PR is whether or not we should clean up the active VMs associated to the provided Ctor. IMO, this is not a problem... yes, you have things in the weakmap that are anchoring those VMs to old versions of Ctor, and probably a new one too after the rehydration... but that should not represent a problem at all... this is a dev mode feature anyway. But if you feel that we should do the clean up, I can do that, it is simpler.
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 do think we should cleanup when possible. The 6 weakmaps makes me nervous about memory leaks.
The three weak maps for the swaps allow circular references. The other three weakmaps create a link between the swaps and the vms. Now, the VMs themselves have a strong reference to the template and the component constructors.
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.
doing it.
|
@ravijayaramappa @diervo this is ready for a final pass! |
| // in the vm, we only hold the selected (already swapped template) | ||
| // but the styles attached to the template might not be the actual | ||
| // active ones, but the swapped versions of those. | ||
| stylesheet = getStyleOrSwappedStyle(stylesheet); |
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.
@ravijayaramappa this is not very elegant. I think we should start storing the styles into the vm, after all the swapping and computations. then here we don't need to do this.
| const elm = createElement('x-simple', { is: Simple }); | ||
| document.body.appendChild(elm); | ||
| expect(getComputedStyle(elm.shadowRoot.firstChild).display).toBe('block'); | ||
| swapStyle(blockStyle[0], inlineStyle[0]); |
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.
@diervo what I noticed is that when you import a css file, what you get is the array, not the factory function. That's why I have to use [0] everywhere here... not ideal. The question is, how does your other part work? is it an array, or each individual one? we can certainly do the thing on the array identity from the import instead of individual style factories.
|
Closing in favor of #2071 |
Details
Experimental template, component and stylesheet hot-swapping for @diervo's crazy idea!
Fix #1869
Does this PR introduce breaking changes?
No, it does not introduce breaking changes.Example of usage