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

fix(engine-core): unrender stylesheets in HMR #4156

Merged
merged 1 commit into from
Apr 22, 2024

Conversation

nolanlawson
Copy link
Contributor

Details

Fixes #4115

This PR makes swapStyle() unrender whatever style is being swapped out. This fixes the issue where stylesheets are only ever appended, never replaced.

swapStyle(A, B) // A is unrendered, B is rendered
swapStyle(B, C) // B is unrendered, C is rendered

The basic invariants of this PR are:

Invariant 1: Do not change how production code works

I don't see a strong reason to change the current production system for rendering stylesheets, since it's heavily optimized. (E.g. we avoid re-rendering the same stylesheet twice, and we prefer constructable stylesheets over <style>s.)

Invariant 2: Avoid creating a difference between production and development, when HMR is not in play

It would be bad if a user tested their component in dev mode and found that it behaved differently in prod mode. This is especially important for stylesheets, which can have quirks due to the insertion ordering or deduplication in case of collisions. (We tell developers not to rely on these quirks, but c'mon, they still do.)

So I avoided making any changes that would cause differences between dev and prod, except when HMR is in play. When HMR is in play, I think it's a little more acceptable for there to be subtle differences. At the end of the day, a developer should refresh the page to see the "real" behavior and not fully trust the HMR'd behavior.

Design decisions

These invariants led to some design decisions:

  1. Collisions between stylesheets are not handled. If two stylesheets have identical CSS content, and they are injected into the same target (e.g. the <head>), then unrendering one causes the other to be unrendered. This is tracked in [HMR] swapStyle() API does not account for stylesheet collisions #4155.
  2. Swapping back and forth multiple times does not work. This is a pre-existing issue, and is tracked in [HMR] swap* APIs show stale content when swapping back and forth multiple times #4154.
  3. Rather than replacing stylesheets in-place, I am merely removing the old style and appending the new one. This can (of course) lead to subtle differences in CSS cascade due to the ordering. But again, I don't think it's important for HMR to work exactly the same as non-HMR. We can revisit this later if/when we address the above issues.

I think these issues are valid, but I wanted to start with a simple PR first.

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.

Not in prod anyway. And not even in dev mode, when HMR is not in play.

GUS work item

W-15347398

@nolanlawson nolanlawson requested a review from a team as a code owner April 16, 2024 21:05
content: string,
target: ShadowRoot | undefined,
signal: AbortSignal | undefined
) => void;
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 decided to use an AbortController because I think it's a nice way to have a clean separation between engine-core and engine-dom. engine-core only ever needs to deal with the AbortController, and engine-dom only ever needs to deal with the AbortSignal. engine-core is responsible for triggering the abort, and engine-dom just needs to respond to the abort event.

In the future, you could imagine abstracting this in such a way that each AbortController is tied to an individual StylesheetFactory rather than just a string; that might help with implementing #4155.

let activeTemplates: WeakMultiMap<Template, VM> = /*@__PURE__@*/ new WeakMultiMap();
let activeComponents: WeakMultiMap<LightningElementConstructor, VM> =
/*@__PURE__@*/ new WeakMultiMap();
let activeStyles: WeakMultiMap<StylesheetFactory, VM> = /*@__PURE__@*/ new WeakMultiMap();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nothing really changed here; I just added PURE annotations because I'm paranoid. I don't think this actually makes a difference, but I prefer to be safe in case bundlers have different heuristics (Rollup vs Webpack vs ESBuild, etc.).

activeComponents = new WeakMultiMap();
activeStyles = new WeakMultiMap();
};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previous tests were not extensive enough to hit cases where you actually need to reset this state properly between tests.

@nolanlawson nolanlawson merged commit 23a7fb7 into master Apr 22, 2024
10 checks passed
@nolanlawson nolanlawson deleted the nolan/swap-styles-squashed branch April 22, 2024 19:43
Copy link
Contributor

@ravijayaramappa ravijayaramappa left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks for making this work!

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.

[HMR] swapStyle() API should swap CSS content rather than appending it
3 participants