Skip to content

Conversation

@caridy
Copy link
Collaborator

@caridy caridy commented May 5, 2020

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

// In this example we use websockets to notify the client that a particular module has changed. We refetch the new modules and ask the engine to hot swap them.

import { swapTemplate, swapStyle, swapComponent } from 'lwc';
export function initHMR(): void {
    const socket = new WebSocket(`ws://${location.host}`);

    socket.addEventListener('message', async ({ data }) => {
        const event = JSON.parse(data);

        if (event.eventType === 'moduleUpdate') {
            const {
                oldUri,
                newUri,
                module: { namespace, name },
            } = event.payload;

            const oldModule = await import(oldUri);
            const newModule = await import(newUri);
            if (name.endsWith('html')) {
                console.log(`Swapping html template for module "${namespace}/${name}"`);
                swapTemplate(oldModule.default, newModule.default);
            } else if (name.endsWith('css')) {
                swapStyle(oldModule.default[0], newModule.default[0]);
            } else {
                swapComponent(oldModule.default, newModule.default);
            }
        } else {
            console.log('ws: ', event);
        }
    });
}

@caridy caridy requested a review from diervo May 5, 2020 18:01
// 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);
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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';
Copy link
Collaborator Author

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.

@diervo diervo changed the title refactor(engine): add support for templat hot swapping refactor(engine): add support for template hot swapping May 5, 2020
@@ -0,0 +1,36 @@
import { createElement } from 'lwc';
Copy link
Contributor

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.

Copy link
Collaborator Author

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)) {
Copy link
Contributor

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.

Copy link
Contributor

@diervo diervo May 14, 2020

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@diervo what @pmdartus is saying is to reload the entire page (which we can definitely do, if the VM doesn't have an owner, which means it is a root element. I can do that for sure.

Copy link
Contributor

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, ...

Copy link
Collaborator Author

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)

Copy link
Collaborator Author

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

@pmdartus
Copy link
Contributor

@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.

@diervo
Copy link
Contributor

diervo commented May 14, 2020

@pmdartus done

@pmdartus
Copy link
Contributor

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 swapTemplate for example with the old template. It appears that other frameworks like Vue are associating swappable modules with a unique identifier to avoid keeping the old reference around.

Let's make sure that those new APIs are compatible with Webpack to leverage them in lwc-create-app.

/cc @muenzpraeger

@diervo
Copy link
Contributor

diervo commented May 15, 2020

@pmdartus can't we just hook in the process so we are the ones keeping track? That what I do in LWR anyway. But moreover the current implementation seems to be working passing always the oldest reference FYI @caridy

@caridy
Copy link
Collaborator Author

caridy commented May 20, 2020

@pmdartus @diervo I have updated the PR with all the suggested fixes... only component and style tests are missing, working on them... can you do another pass?

@salesforce-best-lwc-internal
Copy link

🥳 Performance Improvement

Best has detected that there is a 5.0% performance improvement across your benchmarks.

Please click here to see more details.

Click to view significantly changed benchmarks

lwc-engine-benchmark

✅ Improvements base (76812fb) target (c06d867) trend
table-append-1k.benchmark/benchmark-table/append/1k 334.14 (± 1.98ms) 269.63 (± 2.26ms) -64.5ms (19.3%)
table-clear-1k.benchmark/benchmark-table/clear/1k 17.99 (± 0.20ms) 16.44 (± 0.22ms) -1.6ms (8.6%)
table-create-10k.benchmark/benchmark-table/create/10k 1798.19 (± 18.20ms) 1442.16 (± 14.12ms) -356.0ms (19.8%)
table-create-1k.benchmark/benchmark-table/create/1k 216.75 (± 1.50ms) 176.79 (± 1.03ms) -40.0ms (18.4%)
table-update-10th-1k.benchmark/benchmark-table/update-10th/1k 172.84 (± 2.50ms) 145.44 (± 1.78ms) -27.4ms (15.9%)
tablecmp-append-1k.benchmark/benchmark-table-component/append/1k 432.89 (± 2.47ms) 365.06 (± 2.46ms) -67.8ms (15.7%)
tablecmp-clear-1k.benchmark/benchmark-table-component/clear/1k 10.29 (± 0.15ms) 9.21 (± 0.16ms) -1.1ms (10.5%)

import { Template } from './template';
import { StylesheetFactory } from './stylesheet';

const swappedTemplateMap = new WeakMap<Template, Template>();
Copy link
Contributor

@ravijayaramappa ravijayaramappa May 21, 2020

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.

Copy link
Collaborator Author

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.

@salesforce-best-lwc-internal
Copy link

🥳 Performance Improvement

Best has detected that there is a 6.6% performance improvement across your benchmarks.

Please click here to see more details.

Click to view significantly changed benchmarks

lwc-engine-benchmark

✅ Improvements base (76812fb) target (c447066) trend
table-append-1k.benchmark/benchmark-table/append/1k 334.14 (± 1.98ms) 266.29 (± 1.75ms) -67.9ms (20.3%)
table-clear-1k.benchmark/benchmark-table/clear/1k 17.99 (± 0.20ms) 15.85 (± 0.26ms) -2.1ms (11.9%)
table-create-10k.benchmark/benchmark-table/create/10k 1798.19 (± 18.20ms) 1421.86 (± 14.41ms) -376.3ms (20.9%)
table-create-1k.benchmark/benchmark-table/create/1k 216.75 (± 1.50ms) 173.94 (± 1.54ms) -42.8ms (19.7%)
table-update-10th-1k.benchmark/benchmark-table/update-10th/1k 172.84 (± 2.50ms) 141.27 (± 1.67ms) -31.6ms (18.3%)
tablecmp-append-1k.benchmark/benchmark-table-component/append/1k 432.89 (± 2.47ms) 362.53 (± 2.73ms) -70.4ms (16.3%)
tablecmp-clear-1k.benchmark/benchmark-table-component/clear/1k 10.29 (± 0.15ms) 8.86 (± 0.19ms) -1.4ms (13.9%)

// 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 = () => [];
Copy link
Contributor

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?

Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

@caridy caridy May 22, 2020

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?

Copy link
Contributor

@ravijayaramappa ravijayaramappa May 22, 2020

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);
Copy link
Collaborator Author

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?

Copy link
Contributor

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.

Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doing it.

@caridy
Copy link
Collaborator Author

caridy commented May 28, 2020

@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);
Copy link
Collaborator Author

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]);
Copy link
Collaborator Author

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.

@ravijayaramappa ravijayaramappa added this to the 230 milestone Jun 3, 2020
@diervo
Copy link
Contributor

diervo commented Oct 30, 2020

Closing in favor of #2071

@diervo diervo closed this Oct 30, 2020
@diervo diervo deleted the caridy/template-hot-swap branch November 25, 2020 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for hot-swapping of templates, components, and styles

5 participants