-
Notifications
You must be signed in to change notification settings - Fork 396
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
Conversation
@@ -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()) { |
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 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.
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 The reason is that, in non-SSR, we do two things differently:
- Use Constructable Stylesheets where supported (perf boost)
- 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.
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.
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?.(); |
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 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
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.
attachInternals
is not supported in Safari. Would we need to polyfill 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.
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.
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.
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?
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.
exactly, the same old code.
now, in terms of benefits, two main benefits:
- avoid users to poison the engine, e.g.:
class Foo extends HTMLElement { attachShadow() { return something; } }
. - closed shadows hydration.
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.
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:
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.
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.
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.
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.
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.
All the hydration tests currently fails, because the hydrated elements are never registered as custom elements.
export function attachShadow(element: Element, options: ShadowRootInit): ShadowRoot { | ||
if (hydrating) { | ||
return element.shadowRoot!; | ||
} | ||
return element.attachShadow(options); | ||
const internals = (element as any).attachInternals?.(); |
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.
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?.(); |
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.
All the hydration tests currently fails, because the hydrated elements are never registered as custom elements.
Let's keep this as a draft since there are intersection with other various PRs.
@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. |
|
awesome... I will update this PR. |
Update packages/@lwc/engine-dom/src/renderer.ts
5093d44
to
bb02d34
Compare
Looks like the hydration tests are still failing. |
} | ||
return element.attachShadow(options); | ||
export function attachShadow(element: HTMLElement, options: ShadowRootInit): ShadowRoot { | ||
const internals = element.attachInternals?.(); |
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.
@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.
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 have added the try/catch.
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 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); |
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 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.
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 agree to do the internals on another PR.
closing in favor of #2975 |
Details
If seems that
isHydrating
was used for two main things:attachShadow
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?
Does this pull request introduce an observable change?