-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Remove all hydration wrapper divs via "virtual fragments" #101
Conversation
Co-authored-by: Nate Moore <[email protected]>
const astroId = `${Math.floor(Math.random() * 1e16)}`; | ||
return { ['data-astro-id']: astroId, root: `document.querySelector('[data-astro-id="${astroId}"]')`, Component: 'Component' }; | ||
const createContext = (toHash: string, i: number) => { | ||
const hash = shorthash.unique(toHash); |
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.
👍🏻 Haven’t used shorthash. I’ve been looking for a library that does this, but the ones I found were too weighty. I’d like to use this in other places in Astro too (currently scoped styles uses a custom crypto
function)
const i = (ids.has(value) ? ids.get(value) : -1) + 1; | ||
ids.set(value, i); | ||
const innerContext = createContext(value, i); | ||
value = injectAstroId(value, innerContext['data-astro-id']); |
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 assume this happens on every render. I think the cost of that is too high.
|
||
const fragment = { | ||
parentNode, | ||
firstChild: childNodes.item(0), |
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.
Very clever hack! Does feel a bit fragile. I can think of a bunch of APIs that a framework might use that's not included in this list (lastChild
for example). We're more likely to run into them when we add frameworks (I bet Lit uses more modern apis like append
and prepend
for example), but the worst part is that React or Svelte or whoever might make a patch release that uses one of the APIs not on this list and our stuff would break :(
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.
They might not have been called "islands", but there’s a rich history of doing things like this to hydrate without a parent. 😄
https://stackblitz.com/edit/react-jp5grx?file=index.js
This technique is performs better than rendering into a fragment and using the render callback to update the actual DOM.
1ef0ed1
to
5fe2dbe
Compare
I really appreciate the hack here! It's clever. But I do think the cost is just a bit too high. I can think of a few alternative ideas that might prevent the problems but those all will have their own caveats as well. I'm not sure there is a good solution to this problem. I think probably any solution has some downsides. I'd prefer we went with the solution that kept our JavaScript size small and didn't affect server rendering performance. We can document that dynamic components require a wrapper element, and that that means you can't use sibling selectors in just this one instance. It's a niche enough need that I don't think there will be many complaints. |
Totally fair @matthewp! I agree, it's a niche enough use case that it shouldn't be much of a problem. Definitely not worth introducing performance overhead. The one thing from this exploration that I would still like to apply in another branch is that I really think |
@matthewp I definitely think this is worth pursuing. While on the whole sibling selectors may be niche (I’m not sure how true this is, but I’m willing to accept it for the purposes of this discussion), they’re particularly important for many progressive enhancement approaches—such as using Another reason to avoid wrapper divs is that deeply nested HTML can negatively impact users who depend on assistive tools like screen readers, and is penalized by:
It can also be a confusing DX. Generally speaking we don’t expect declarative component libraries to produce markup that wasn’t declared. All of these factors motivated me to stop using Emotion for styling. And particularly the progressive enhancement issues would make it difficult for me to use Astro. All of that said, I don’t have much context to understand where the costs are incurred or why, but if you wouldn’t mind elaborating I’d be happy to add another mind to the hive to help find a way to make this happen. I have already proposed a few solutions for issues with Microsite’s approach in natemoo-re/microsite#150. |
@natemoo-re Yeah, hashing based on the HTML contents makes sense, and I do like keeping it deterministic. Will prevent cache busting every page on each deploy. |
@eyelidlessness I don't think it's common to use a sibling select to select something inside of a component. A component is supposed to be a tool for encapsulation. If you use a sibling selector like @natemoo-re's you are going to select the wrapper div. And for a lot of use cases that's perfectly fine. If you need to select the component's top elements you can just change your selector to select them. The workarounds are minor. Lots of software has caveats, this is a pretty acceptable one imo. |
Awesome. I'll spin that deterministic rendering fix out into a separate branch. I'll keep this PR open for a short time to facilitate discussion, but we won't be merging it. IMO the best possible solutions here (in order) would be:
This hack is way down that list. It makes sense not to bake this into Astro. |
@matthewp The issue isn’t selecting into a component, you’re right that it’s trivial to do, say, This could potentially be worked around in some cases by structuring the |
I had an alternative idea that might be worth exploring... We could potentially build a single app per-framework which uses No idea if it's feasible, just a thought experiment for now. |
@eyelidlessness That's a good point. I wonder if this is something we could facilitate with our CSS implementation. For example we could attach a class name to the wrapper element (I would prefer this be a custom element rather than a div but that's another topic). You could use that class name to perform sibling selection. The tough part is that you would need to know the name of that class and it would have to be distinct to only that element instance. Maybe the astro Id could be used for this some how? |
Hmm, not suggesting that we overload a real thing, but maybe we could expose something like |
@matthewp @natemoo-re Unfortunately that wouldn’t work. Pseudoclass selectors (and any selectors) can’t break out of their containing hierarchy. So you can’t do something like |
I blame jQuery for this being hard to discuss by inventing selectors that had a DOM/xpath basis but no basis in standards reality |
Closing this PR as it won't be merged. If warranted, further discussion can take place in an issue. |
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Changes
This PR removes wrapper
<div>
elements from client-side Components. The HTML you write is the exactly the HTML that is generated. Why? Because people will try to style client-side components with:nth-child
and> * + *
selectors, but we're opaquely injecting a wrapper<div>
around each one, breaking their assumptions.Frameworks require you to pass a parent
Element
for mounting. Your app's markup will be rendered as its children. Requiring a wrapper<div>
seems like a technical limitation, but this PR introduces a way to sidestep it.By passing what I'm calling a virtual fragment, we can control framework rendering behavior by creating a
Proxy
which safely overrides a few specific DOM methods. These overridden methods narrow the DOM elements controlled by the framework from allchildren
to only a subset ofchildren
.Testing
Check the output of
examples/kitchen-sink
—no wrappers!Docs
Note We'll still need to hoist all Astro setup scripts to the
<head>
or end of<body>
, which needs to happen in the compiler.optimize
runs before these scripts are generated, so we'll need a new hook.