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): removing isHydrating global flag used across packages #2806

Closed
wants to merge 3 commits into from

Conversation

caridy
Copy link
Contributor

@caridy caridy commented Apr 26, 2022

Details

If seems that isHydrating was used for two main things:

  1. to know whether or not to call attachShadow
  2. to treat style nodes differently

For 1, we can solve that using element internals, since IE11 doesn't support SSR, we can rely on internals or attach a new shadow, and not having to observe anything about rehydration.

For 2, I don't know what the deal is.

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.

@@ -209,12 +208,8 @@ export function createStylesheet(vm: VM, stylesheets: string[]): VNode | null {
for (let i = 0; i < stylesheets.length; i++) {
insertGlobalStylesheet(stylesheets[i]);
}
} else if (ssr || isHydrating()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the concerning part. It seems that during rehydration, the style vnode is very different from a regular rendering, and the diffing algo has special behavior for that... I don't know what the deal is... but I will like to know.

If it turns out that this is the case, maybe then the hydration process will NOT necessary try to preserve the original style, but just go with the replacement of it since that will NOT blink due to the insertion/removal.

Copy link
Collaborator

@nolanlawson nolanlawson Apr 27, 2022

Choose a reason for hiding this comment

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

@caridy The reason is that, in non-SSR, we do two things differently:

  1. Use Constructable Stylesheets where supported (perf boost)
  2. Hoist style nodes to the nearest native shadow root (e.g. for light DOM styles) (also a perf boost)

Neither is possible in SSR. (The first because there's no browser standard for it, and the second because we only do one pass through the DOM tree during the rendering.) So we just emit inline <style> nodes.

For hydration, we can potentially work around this. When hydrating, we can just remove the inline <style>s and do our normal flow. I'm also working on a separate PR to properly remove stylesheets when unused: #2797.

Copy link
Collaborator

Choose a reason for hiding this comment

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

FWIW in #2797 I am also getting rid of this usage of isHydrating, but for a different reason. So if that PR gets merged first, then you won't have to remove it here.

return element.shadowRoot!;
}
return element.attachShadow(options);
const internals = (element as any).attachInternals?.();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the most important part of the PR. We rely on attachInternals to access any shadowRoot (open or closed), and that will take care of returning the right shadow. This method in general should probably be renamed in the future to something more like: attachShadowIfNeeded

Copy link
Collaborator

@nolanlawson nolanlawson Apr 27, 2022

Choose a reason for hiding this comment

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

attachInternals is not supported in Safari. Would we need to polyfill this?

Copy link
Contributor Author

@caridy caridy Apr 28, 2022

Choose a reason for hiding this comment

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

no, not in this case. In the previous incarnation of this code, the option was always to assume that an open shadow was created, and therefor the .shadowRoot value was there. This one assumes that attachInternals might not be there, and then use the dot notation to read open shadowRoots, and only at that point, decide to attach it. Basically, the new code covers more ground than the original code for closed shadows.

Copy link
Collaborator

Choose a reason for hiding this comment

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

So if we assume we're running in Safari, then this code would boil down to:

return element.shadowRoot || element.attachShadow(options)

Can we use this instead of attachInternals? What benefit do we get from using attachInternals in Firefox and Chrome?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

exactly, the same old code.

now, in terms of benefits, two main benefits:

  1. avoid users to poison the engine, e.g.: class Foo extends HTMLElement { attachShadow() { return something; } }.
  2. closed shadows hydration.

Copy link
Collaborator

Choose a reason for hiding this comment

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

For # 1, couldn't users poison the engine using class Foo extends HTMLElement { attachInternals() { return something; } } instead?

For # 2, I'm a bit surprised because it seems attachInternals defeats the whole purpose of closed shadow DOM. But I confirmed that you're right, you can access the shadow root that way:

Screen Shot 2022-04-28 at 3 50 22 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, for #1, LWC is NOT protecting against poisoning. LWS does everywhere. You don't protect about them providing attachShadow() jejejeje. You can simply cache the method, and call it to avoid dot notation, but again, that's not a thing... it was a some point, but for some reason, that was removed everywhere.

For #2, NO, attachInternals can only be called during the construction phase... after that it throws.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Based on my testing (see screenshot above), attachInternals can be called after the constructor phase. But it can't be called twice. So I guess a closed-shadow-root component would want to call it during the constructor phase so that someone else can't get access to its shadow root.

If attachInternals is not supported in Safari, and if there's no advantage in Chrome/Firefox to using it, then I kinda feel we are better off avoiding it… otherwise we have the added complexity of 2 code paths for 2 different browsers, with no real advantage.

Copy link
Member

Choose a reason for hiding this comment

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

All the hydration tests currently fails, because the hydrated elements are never registered as custom elements.

Comment on lines 192 to 193
export function attachShadow(element: Element, options: ShadowRootInit): ShadowRoot {
if (hydrating) {
return element.shadowRoot!;
}
return element.attachShadow(options);
const internals = (element as any).attachInternals?.();
Copy link
Member

Choose a reason for hiding this comment

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

You can remove the any if you change the element type from Element to HTMLElement.

export function attachShadow(element: HTMLElement, options: ShadowRootInit): ShadowRoot {
    const internals = element.attachInternals?.();

return element.shadowRoot!;
}
return element.attachShadow(options);
const internals = (element as any).attachInternals?.();
Copy link
Member

Choose a reason for hiding this comment

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

All the hydration tests currently fails, because the hydrated elements are never registered as custom elements.

@caridy caridy marked this pull request as draft May 3, 2022 20:13
@caridy
Copy link
Contributor Author

caridy commented May 3, 2022

Let's keep this as a draft since there are intersection with other various PRs.

All the hydration tests currently fails, because the hydrated elements are never registered as custom elements.

@pmdartus that is precisely one of the problems here. It is a gap in our tests, but let me explain in details what is that a problem:

If you have a component A that is being hydrated from SSR, and that component uses component B and C in its shadow root content (also from SSR) - in that order, when component B is going to be hydrated, an error of some sort occur, e.g.: mismatch or the content of B's shadow is just empty and will be rendered on the first pass, and it uses C inside B, which is not going to be hydrated, but created, it is going to get registered. So, by the time you get to C inside A, C was already registered. I hope that this explains the unpredictable aspect of the current code. This PR can solve that because it doesn't matter when something is registered, but when the engine attempts to upgrade it instead.

I need some help to investigate what's going on here.

@nolanlawson
Copy link
Collaborator

isHydrating is no longer used in stylesheet.ts now: #2827

@caridy
Copy link
Contributor Author

caridy commented May 12, 2022

isHydrating is no longer used in stylesheet.ts now: #2827

awesome... I will update this PR.

@caridy caridy force-pushed the caridy/remove-is-hydrating-global-flag branch from 5093d44 to bb02d34 Compare May 12, 2022 20:06
@nolanlawson
Copy link
Collaborator

Looks like the hydration tests are still failing.

}
return element.attachShadow(options);
export function attachShadow(element: HTMLElement, options: ShadowRootInit): ShadowRoot {
const internals = element.attachInternals?.();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nolanlawson yes, as @pmdartus pointed out, the problem persist on this line. The fact that tagNames are not registered during rehydration makes this line to throw when attachInternals is present, but the element was not upgraded by the UA. I don't have a solution just yet for this problem, in another PR I'm proposing to always register before any form of upgrading, but this PR should get in first... maybe just adding a try/catch for now and eventually removing the try/catch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added the try/catch.

@caridy caridy marked this pull request as ready for review June 3, 2022 22:42
Copy link
Collaborator

@nolanlawson nolanlawson left a comment

Choose a reason for hiding this comment

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

I looked into this and it seems to me that isHydrating is not necessary. And in fact it causes problems for me in pivots (#2564), so I need to get rid of it anyway (AFAICT).

Just one small nitpick and then this LGTM. @caridy

Comment on lines +120 to +127
let internals: ElementInternals | undefined;
try {
internals = element.attachInternals?.();
} catch {
// unregistered elements will throw, in which case we go with plan B
// to inspect the open shadowRoot on the element.
}
return element.attachShadow(options);
return internals?.shadowRoot || element.shadowRoot || element.attachShadow(options);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would suggest replacing this function with:

    if (!isNull(element.shadowRoot)) {
        return element.shadowRoot;
    }
    return element.attachShadow(options);

The attachInternals() is not strictly related to this PR and can be handled in a follow-up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree to do the internals on another PR.

@caridy
Copy link
Contributor Author

caridy commented Aug 3, 2022

closing in favor of #2975

@caridy caridy closed this Aug 3, 2022
@ravijayaramappa ravijayaramappa deleted the caridy/remove-is-hydrating-global-flag branch December 13, 2022 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants