Skip to content
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

refactor(engine-core): POC splitting up diffing from rehydration #2608

Closed
wants to merge 17 commits into from

Conversation

pmdartus
Copy link
Member

Details

This PR is a proof-of-concept for splitting up the diffing logic from the rehydration logic. VNode hooks are mixing up today both diffing and hydration logics. By removing the concept of hooks, it becomes quite straightforward to split up to the logic.

The VNode hook field can be replaced with a type field. The type field is a new field present on all VNodes. It is used to invoke the appropriate mounting or patching function. One of the nice side-effects of this change is that it makes the diffing algo code way easier to follow and resonate about. With this change the code is now broken down:

  • src/framework/vnode.ts: VNode type definition
  • src/framework/rendering.ts: Component mounting and diffing logic with patchChildren for entry point
  • src/framework/hydration.ts: Component rehydration logic with hydrateChildren for entry point

Here are some other observations related to this change:

  • This code path in src/framework/modules/events.ts is never invoked.
  • The existing hooks.ts contains some duplicated code that can be simplified.
  • I ran some performance tests on VDOM traversal and it appears that JavaScript engines are capable to traverse deep VDOM trees quite fast. I am not convinced that the added complexity introduced by VM.velements is worth the performance improvement.
  • Remove the VNode.owner is not as simple as what I originally thought, mainly because of slotted content.

Does this pull request introduce a breaking change?

  • ✅ No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • ✅ No, it does not introduce an observable change.


function throwHydrationError() {
assert.fail('Server rendered elements do not match client side generated elements');
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

One benefit of having hydration in one place is that we can potentially remove this code for environments that don't use hydration. Svelte has a hydratable option that it uses for such optimizations: sveltejs/svelte#6525

@nolanlawson
Copy link
Collaborator

nolanlawson commented Dec 16, 2021

Remove the VNode.owner is not as simple as what I originally thought

Merging #2598 will make it easier to remove owner, because there's no more need for owner.renderer. 🙂

break;

case VNodeType.CustomElement: {
const vm = getAssociatedVMIfPresent(vnode.elm);
Copy link
Contributor

Choose a reason for hiding this comment

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

@pmdartus in your original refactor, we were doing vnode.vm, and I changed it back to getAssociatedVMIfPresent. What get's my attention is that it only was observable on the hydration tests. I can say for sure out the top of my mind, but maybe we should set a VM in the vnode as part of the hydration process?

Copy link
Contributor

@jodarove jodarove left a comment

Choose a reason for hiding this comment

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

@pmdartus first pass, all looking good. i still need to dive into patch/mount(Element/customElement)

ctor: Ctor,
owner: vmBeingRendered,
mode: 'open', // TODO [#1294]: this should be defined in Ctor
vm,
Copy link
Contributor

Choose a reason for hiding this comment

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

where is this vm being set?

Comment on lines +100 to +101
function processElement(vnode: VElement, node: Node) {
const elm = node as Element;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
function processElement(vnode: VElement, node: Node) {
const elm = node as Element;
function processElement(vnode: VElement, elm: Element) {

break;

case VNodeType.Element:
processElement(vnode, node);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
processElement(vnode, node);
processElement(vnode, node as Element);

Comment on lines +132 to +134
function processCustomElement(vnode: VCustomElement, node: Node) {
const elm = node as Element;

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
function processCustomElement(vnode: VCustomElement, node: Node) {
const elm = node as Element;
function processCustomElement(vnode: VCustomElement, elm: Element) {

break;

case VNodeType.CustomElement:
processCustomElement(vnode, node);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
processCustomElement(vnode, node);
processCustomElement(vnode, node as Element);

renderer.addEventListener(elm, name, listener);
for (const name in on) {
const handler = on[name];
renderer.addEventListener(elm, name, (event: Event) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Some questions:

  1. before we used to have 1 handler for all events, now we have 1 handler per event (as many handlers as events). Pls, confirm.
  2. why can we skip on && on[type] check? because it will always be defined? is the handler is our own invokeEventListener from the bind function?
  3. why not just do renderer.addEventListener(elm, name, handler);?

}
}

function patch(n1: VNode | null, n2: VNode, parent: ParentNode, anchor: Node | null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@pmdartus something feels off about this function. basically is doing 2 different things based on the parameters. if n1 is null, then is mounting (create+insert right?), otherwise patching (I'm still trying to figure out the fix me comment). Then in the process* we make the same differentiation with a condition for isNull (mount/patch).

What do you think if we separate the patch logic into patch and mount, and also the process* into patch* and mount* (you already have them for Element and CustomeElements)? (imho) Such a way is clearer, and we can skip the multiple isNull check which are on the critical path of the diffing algo.

} else {
n2.elm = n1.elm;

// FIXME: Comment nodes should be static, we shouldn't need to diff them together. However
Copy link
Contributor

Choose a reason for hiding this comment

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

true, but, iirc text and comments do not have keys. I'm thinking of foreachs, maybe ifs (this is the same as texts), in which the diff may reuse other vnodses. we diff the texts so we do not need to access the DOM API.

// FIXME: Comment nodes should be static, we shouldn't need to diff them together. However
// it is the case today.
if (n2.text !== n1.text) {
updateTextContent(n2);
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the abstraction. don't know if will help but here is super micro-optimization: we could pass (node, renderer, text) and avoid destructing the vnode in updateTextContent. we have all those here.

Copy link
Contributor

@jodarove jodarove left a comment

Choose a reason for hiding this comment

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

couldn't find any issues in the individual hooks migrations. just one question

throw new TypeError(`Incorrect Component Constructor`);
}

applyStyleScoping(elm, vnode);
Copy link
Contributor

Choose a reason for hiding this comment

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

applyStyleScoping was done within the callback to UpgradableConstructor (part of createViewModelHook). Do you foresee any issues moving it outside?

@nolanlawson
Copy link
Collaborator

Sorry for bitrotting this PR; I really like the direction of getting rid of hooks.

@pmdartus
Copy link
Member Author

Thanks for the feedback @jodarove. There is a lot going on in the core engine these days, it will be better to go with an incremental approach to ease the review process and avoid merge conflicts. I will try to break this change into smaller chunks. Here is the first PR: #2630

@pmdartus
Copy link
Member Author

pmdartus commented Feb 4, 2022

Superseded by #2677

@pmdartus pmdartus closed this Feb 4, 2022
@ravijayaramappa ravijayaramappa deleted the pmdartus/renderer-refactor branch July 22, 2022 17:27
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.

3 participants