whatwg/dom

Declarative Shadow DOM

mfreed7 opened this issue Β· 276 comments

I would like to re-open the topic of declarative Shadow DOM. This has been discussed in the past, here on WHATWG, in W3C here and here, and in WICG. The last substantive public discussion was at the Tokyo Web Components F2F, where it was resolved not to proceed. I would like to revisit that decision.

I think declarative Shadow DOM is an important feature that is missing from the Web, and is something that we should try to implement. The primary motivating use case for declarative Shadow DOM is Server Side Rendering (SSR), which is practically difficult or impossible to use in combination with Shadow DOM. There are also other compelling use cases such enabling scoped styles without requiring Javascript. The rationale behind the prior decision not to proceed with this feature was largely a) implementation complexity and b) lack of developer need.

To address these points, and to explore the topic further, I've written up an explainer, here:

https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md

I believe this document captures most of the details of the motivation, required features, contentious points, and prior history. But I would love to hear your thoughts and feedback so that this proposal can evolve into something implementable and standardizable. I'm hoping we can use this thread as a discussion forum.

As a quick summary of the proposed syntax, this HTML:

<host-element>
    <template shadowroot="open">
        <style>shadow styles</style>
        <h2>Shadow Content</h2>
        <slot></slot>
    </template>
    <h2>Light content</h2>
</host-element>

would be parsed into this DOM tree:

<host-element>
  #shadow-root (open)
    <style>shadow styles</style>
    <h2>Shadow Content</h2>
    <slot>
        ↳ <h2> reveal
    </slot>
  <h2>Light content</h2>
</host-element>

One interesting question about the proposal is how does it affect all the other weird html parsing things like table fixups and what not. If I have <template shadowroot="open"><td>Foo</td></template>, what is the final dom?

(Other than that kind of stuff, I agree that this is something worth addressing)

Also, it seems a bit weird/unfortunate that you are forced to have a template for every shadow host, when I assume the common thing for a given component is to always have the same shadow root... But I don't have a great solution for that off-hand, maybe you should be able to reference a template from the host by ID? Something else?

cc @hsivonen @smaug---- @EdgarChen

Well I guess the parsing insertion may or may not be much of an issue, as you don't have to insert the template contents in the parent, but instead goes directly into the shadowroot...

There's still a risk here in that a previous harmless template can now be used for script injection if you can do some attribute injection. (Also, browsers continue to have security issues around template elements to this day, which isn't reassuring.)

It'd be good to complete the algorithm so it deals with the element already having a shadow root and it details what "moving" means.

I wonder if converting Firefox UI code from XBL to more web component-y has brought up any ideas related to this issue.
@bgrins

I wonder if converting Firefox UI code from XBL to more web component-y has brought up any ideas related to this issue.

I don't think this would make sense for the Firefox frontend. That said, it seems like we aren't the target audience because of https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md:

The entire motivation for this feature is no-JS environments

We don't have to support no-js environments or Server Side Rendering at all. We also don't currently use Shadow DOM outside of Custom Elements anywhere (it's possible we may want to do it sometime, but because we always have JS we'd probably just make it a Custom Element in that case). In addition there are some things from my reading of the proposal that would make it inconvenient for our use cases (specifically with shared widgets), and might also be inconvenient for sites that do want to support SSR:

  1. Many of our Custom Elements are used many times throughout a single document, so this would require duplication of the template. For instance we have a couple hundred menuitems and toolbarbuttons in the DOM in browser.xhtml at startup.
  2. Some Custom Elements are used in a lot of documents across the tree or are used inside of other Custom Elements. In these cases the duplication would be spread across multiple files. This would still require duplication even in the "Instead of inline contents, use an idref to an existing template" alternative.
  3. Many Custom Elements get created from JS so AIUI we'd need to programmatically insert the template, or create the same shadow content from JS in another way.

FWIW: what we do now is more-or-less:

  • Add a static string getter for markup in an Custom Element class
  • Pass that into DOMParser.parseFromString to get a DocumentFragment
  • Cache the fragment and do essentially this.shadowRoot.appendChild(document.importNode(fragment, true)) in the constructor or connectedCallback.

I have sort of wished in the past we could have a more declarative way to define the markup in (x)html files alongside scripts and styles, so it's nice to see this being explored though. @mfreed7 I'd be interested to hear more about this point:

Why not wait for, or link this to, declarative custom elements? At first blush, it would seem that these two proposals go together. However, the primary motivating use case for declarative Shadow DOM is SSR and No-JS. Custom element definitions need javascript to function; therefore, this is a different use case/proposal and the two should not be tied together

Specifically if there's a reason that SSR tools couldn't/shouldn't be taught to parse a syntax like that to work even in an environment without JS? So you could declare a custom element with only a template, then have a tool end up creating the same output they would with Declarative Shadow DOM. I'm not familiar with the tooling here, so it's possible I'm missing something.

I mean, it seems to me what we really need is a declarative way to instantiate at template at which case we could potentially provide some directive to that node that renders the template inside a shadow DOM.

I love this idea, but adding a shadowroot attribute to an HTMLTemplateElement completely changes the semantics of the element. We should have something like <target template="templateID"> that would clone and insert the template’s contents declaratively. Of course that introduces a scoping problem that would need to be figured out since ids are scoped to the shadow root.

Would there be a way to share the the shadow template and/or style across multiple instances of a host-element?

Hi, this should not be standard, since it is rather a hydration technique that can be easily adopted by any library, please analyze the example attached in the documentation, it should generate the polyfill in connectedCallback and not in the constructor.

I think the standards associated with the web should not cover the SSR, this is the work of libraries.

Summary: as a technique it is excellent and I think I'll adopt it atomico , but not this should be a standard

@emilio @calebdwilliams and @vikerman I think standardized templating, along the lines of Template Instantiation is a very valuable feature to explore, but it's a bit different than declarative shadow roots.

I look at this proposal mainly as a way to re-establish the ability to meaningfully serialize a DOM tree (now that include shadow roots). There are a few applications this addresses, including SSR, and some of them overlap with declarative custom elements and templating, but not all of them. I think it's better to keep these proposals separate for now.

For me, I think the "slot hoisting" is weird and/or counterintuitive. I also wonder what happens when I need 10x <host-elements> on the page. Do I provide 10x styles and and templates and slots?

Not to bikeshed too much, but I think lots of people would prefer auto-instantiating Custom Elements from a single template like...

<template customelement="host-element" shadowroot="open">
  <style>shadow styles</style>
  <h2>Shadow Content</h2>
  <slot></slot>
</template>

<host-element>
  <h2>Light Content</h2>
</host-element>

Then from a SSR authoring standpoint, I could <?php include('host-element.php') ?> once at the top of my document and author freely.

@davatron5000 what do you mean by "slot hoisting"?

Instantiating declarative custom elements is definitely good future work, but it requires a lot more than what you've sketched to be practically useful.

The key thing to understand here is that this lets us serialize instances of shadow roots. Most shadow roots of the same class of component are not identical - they have been produced by some kind of templating layer or DOM manipulation that makes each instance unique. So you usually can't simply refer to a template and stamp that out, you would need to provide it data and enable the template to specify the transform from data to actual DOM. Again, great future work and where the template instantiation and declarative custom elements ideas/proposals are pointing, but quite a bit different from this.

Even once we have declarative custom elements, it's quite likely that this proposal will be needed as is, since for serialization purposes we'll still need to describe the actual shadow root state of the particular instances in cases where we don't have the data that produced the DOM yet, or in the numerous cases where a shadow root wasn't produced by a declarative custom element.

I'm still somewhat on the fence about whether or not we should be able to refer to a pre-existing template so we can avoid repetition.

One of the base use-cases - getting "scoped styles" for a section of your page using the shadow DOM composition boundary without needing JS - is satisfied without that. If you're hand-authoring a no-JS page, you have to repeat all your structure for each element; this doesn't change anything about that, it just adds a little bit more text to each repetition to establish the boundary.

Another satisfied base use-case is shipping server-rendered HTML using shadows that'll be hydrated into full JS-driven custom elements later. You can write custom elements (without having to repeat the contents each time) on your server, then serialize them out into this form; compression should take care of most of the cost of repetition, and post-parsing DOM sizes are comparable.

The use-case not satisfied is wanting to get the less-structural-repetition benefit of a custom element without requiring JS if all you're doing is filling in some DOM and nothing else. That's a reasonable case, I think! But also a less important case than the two I mentioned above. I think if we go without that for now, we're not blocking ourselves from having such a solution later, such as having a <template use=#id></template> that lets it refer to templates already in the DOM? And avoiding that for now lets us skip some more complex scenarios, making the MVP here easier to define.

I was asked to offer feedback on this proposal in my capacity as a framework author, to help ensure that these additions are relevant to those of us not currently using web components. Let me first say that I'm glad the no-JS use case is being taken seriously β€” the lack of SSR support (various WC framework hacks notwithstanding) has made web components a non-starter for many of us.

I have a few questions and observations. Most importantly, I agree with @annevk that it's essential to clarify what happens when declarative and programmatic shadow roots collide. Is this.attachShadow(...) an error if there's already a declarative shadow root? Because that would likely cause all sorts of problems.

Is the expectation that custom element authors would do this sort of thing?

class Clock extends HTMLElement {
  constructor() {
    super();

    if (this.shadowRoot) {
      // declarative shadow root exists
      this.hours = this.shadowRoot.querySelector('.hours');
      this.minutes = this.shadowRoot.querySelector('.minutes');
      this.seconds = this.shadowRoot.querySelector('.seconds');
    } else {
      // declarative shadow root doesn't exist
      this.attachShadow({ mode: 'open', serializable: true });
      this.hours = document.createElement('span');
      this.hours.className = 'hours';
      this.minutes = document.createElement('span');
      this.minutes.className = 'minutes';
      this.seconds = document.createElement('span');
      this.seconds.className = 'seconds';

      this.shadowRoot.append(
        this.hours,
        document.createTextNode(' : '),
        this.minutes,
        document.createTextNode(' : '),
        this.seconds
      );
    }
  }

  connectedCallback() {
    this.update();
    this.interval = setInterval(() => {
      this.update();
    }, 1000);
  }

  disconnectedCallback() {
    clearInterval(this.interval);
  }

  update() {
    const d = new Date();
    this.hours.textContent = pad(d.getHours());
    this.minutes.textContent = pad(d.getMinutes());
    this.seconds.textContent = pad(d.getSeconds());
  }
}

Importantly, this doesn't handle the case where the declarative shadow DOM is malformed for whatever reason (a different version of the custom element, for example), so in reality the code would likely be more complex.

Furthermore, in the (probably fairly common) case that the shadow root is populated via innerHTML, we would find ourselves nuking the existing shadow DOM rather than gracefully hydrating it, which seems like it could have negative consequences (performance, but also blowing away state in <input> elements and so on).

In other words, it's hard to see how we can introduce declarative shadow DOM without introducing significant new complexities for custom element authors.

Duplication of content and styles

As @davatron5000 and others have noted, it looks as though this proposal results in duplication of styles and content. But I don't think it's practical to share a <template> between separate instances because the shadow DOM will often differ. Imagine the clock example above also accounted for timezones, and came with styles β€” the serialized result of using it might look like this:

<p>The time in London is
  <world-clock timezone="GMT">
    <template shadowroot="open">
      <style>
        span {
          font-variant: tabular-nums;
        }

        .seconds {
          font-size: 0.8em;
        }
      </style>

      <span class="hours">12</span> :
      <span class="minutes">34</span> :
      <span class="seconds">56</span>
    </template>
  </world-clock>
</p>

<p>The time in New York is
  <world-clock timezone="EDT">
    <template shadowroot="open">
      <style>
        span {
          font-variant: tabular-nums;
        }

        .seconds {
          font-size: 0.8em;
        }
      </style>

      <span class="hours">07</span> :
      <span class="minutes">34</span> :
      <span class="seconds">56</span>
    </template>
  </world-clock>
</p>

<p>The time in Hong Kong is
  <world-clock timezone="HKT">
    <template shadowroot="open">
      <style>
        span {
          font-variant: tabular-nums;
        }

        .seconds {
          font-size: 0.8em;
        }
      </style>

      <span class="hours">20</span> :
      <span class="minutes">34</span> :
      <span class="seconds">56</span>
    </template>
  </world-clock>
</p>

By contrast, here's what you might get with a non-web-component framework:

<style>
  span.svelte-xyz123 {
    font-variant: tabular-nums;
  }
  
  .seconds.svelte-xyz123{
    font-size: 0.8em;
  }
</style>

<p>The time in London is
  <span class="svelte-xyz123">18</span> :
  <span class="svelte-xyz123">59</span> :
  <span class="seconds svelte-xyz123">36</span>
</p>

<p>The time in New York is
  <span class="svelte-xyz123">13</span> :
  <span class="svelte-xyz123">59</span> :
  <span class="seconds svelte-xyz123">36</span>
</p>

<p>The time in Hong Kong is
  <span class="svelte-xyz123">02</span> :
  <span class="svelte-xyz123">59</span> :
  <span class="seconds svelte-xyz123">36</span>
</p>

Clearly, the non-custom-element version results in many fewer bytes, and a less complex (i.e. more memory-efficient) DOM.

Serialization

I don't think it makes sense for components to declare their shadow roots to be serializable. For one thing, it's unfortunate if serializable: true, which is presumably the intended default, is something you have to opt in to, though the web compat argument is obviously persuasive.

But more to the point, it's not the component's job to determine that. Whether or not shadow DOM should be serialized is a decision that should be taken at the point of serialization, i.e. by the component consumer. In other words, something like this (after a round of bikeshedding) would make a lot more sense to me:

const html = element.innerHTMLWithShadowDOM;

Intended use case

I expect most people are in agreement about this, but I haven't seen it explicitly addressed, so I'll note it here: we're probably not expecting people to write declarative shadow DOM by hand. That would defeat much of the point of web components, which is to encapsulate the component's behaviour in such a way that HTML authors don't need to worry about it, and would vastly increase the likelihood of errors.

Which is to say that this is a capability directed at frameworks. But this means that those frameworks will, in order to take advantage of this for server-side rendering, need to implement a declarative-shadow-DOM-aware DOM implementation that runs in Node.js (or wherever). Such things add non-trivial complexity, and even performance overhead, to something that is today accomplished using straightforward string concatenation.


In summary, while I welcome this discussion, I fear that declarative shadow DOM only gets us part way to what we can already do without web components, but at the cost of additional complexity.

@justinfagnani I'm probably not describing it well, but the Light DOM getting consumed by a sibling element (getting "hoisted up" into the slot) was somewhat confusing. I know the sibling <template> is being converted into Shadow DOM and then the Light DOM is being revealed, but it wasn't very intuitive.

If this is a stepping stone towards something great, then I can support that but Rich's summary is pretty spot on for me (except that I want to be able to hand-author stuff).

Thanks to everyone for the great comments here. There seem to be a few themes - I'll try to summarize and respond:

  1. What should the custom element definition look like?

    The explainer does have a section for this, but I really like the example provided by @Rich-Harris in this comment. That is exactly what I was envisioning - a small if (this.shadowRoot) block that just hooks things up if the shadowRoot already exists, and the actual construction code if not. That code works on both client and server (it is isomorphic), and the added code is minimal. @Rich-Harris asked what happens if the declarative content is malformed - if that is a possibility (due to versioning, etc.), then no matter what the declarative solution, you'll need to do extra work. And in the case where you can assume that an existing #shadowroot means your content is "good to go", you'll get a performance win from not having to blow away the existing content and re-create it.

  2. Wouldn't it be better to re-use a single <template> rather than duplicating it for each shadow root?

    Several people already responded to this, but I wanted to point out this section of the explainer that discusses this point at length. The important three points in my mind are:
    a. As @Rich-Harris and @justinfagnani point out, it is important to remember that we're serializing instances of elements, which likely differ from one another slightly in terms of their DOM content.
    b. In terms of data/overhead, gzip nicely fixes most of the ills of almost-perfectly-repeated content. Aside from the potentially-shared styles (see the point below), you'll get another copy of the DOM no matter what you do here. So the "overhead" benefits of sharing a single <template> for this seem rather limited.
    c. Re-using a <template> like this requires a solution to the previously-unsolved "idref" issue. See here for the discussion around ARIA labelled-by. The problem is: how do you deal with nested shadow roots? The idref would then need to cross shadow bounds, potentially in both directions. We don't have a way to allow that, yet.

  3. How to handle styles?

    This is definitely an open question. I'm hoping we can come up with a declarative Shadow DOM solution that isn't tied to a particular solution to the styling problem. To do that, I have proposed just using inline <style>s within each shadow root. As mentioned, this would result in a) more bytes on the wire, and b) more DOM memory used. Of those, I'm least concerned with a). For the example HTML provided in this comment, when gzipped, the inline <style> example takes 290 bytes, while the "shared stylesheet" example takes 223 bytes. Yes, that's 25% more, but not the factor of two that it would appear from the raw HTML. I agree that problem b) is a problem that needs a solution. Perhaps the parser could detect exactly-duplicate <style> elements and condense them into a single CSSStyleSheet that gets added to adoptedStylesheets? That might be crazy. I do agree that styles need a solution. I don't think it needs to be solved for this declarative Shadow DOM solution to be useful as-is.

  4. Serialization and the "serializable:true" option.

    I love the @Rich-Harris suggestion to add another API (element.innerHTMLWithShadowDOM) that serializes all shadow roots by default. That avoids the need to retrofit existing components with serializable:true, and as you pointed out, this isn't the component's decision to make anyway. I'd be in favor of changing the explainer to match this suggestion.

  5. What about existing html parser behaviors, e.g. table fixup: <template shadowroot=open><td>Foo</td></template>

    The advantage of this proposal is that nearly all of the existing "standard" <template> behavior still applies. For example, for this specific example, the "in template" insertion mode rules apply. I don't think there is any additional ambiguity created by this proposal.

  6. Is script injection a problem? Can't a previously-harmless <template> be made active by attribute injection?

    No, at least according to the existing proposal. This would be a "parser-only" feature, and adding the shadowroot attribute to an existing <template> would have no effect. As pointed out by @hayatoito, you could still imperatively build a <template>, add a shadowroot attribute, and then do element.innerHTML = element.innerHTML. The innerHTML assignment would see the full <template shadowroot> and would attach a shadow as it is parsed. But that doesn't seem like a security risk, since you're blowing away the entire innerHTML in that case anyway. Please correct me if I'm wrong.

  7. It is weird to have a <template shadowroot> turn into a #shadowroot and then have previously-sibling elements get slotted into the #shadowroot.

    Yes, this is definitely different and will take getting used to, no question. But this statement seems like it would apply to any declarative Shadow DOM solution. No matter the semantics, some element will become, or create, a #shadowroot which will then start "pulling in" sibling content into <slot>s.

  8. What happens if there is already a shadow root?

    For compatibility and alignment, this needs to be an error. I mentioned several such scenarios in the explainer, here. Basically, you can (still) only attach a shadow root once, and any subsequent attempts (either declarative or imperative) will result in an error.

  9. Adding shadowroot to <template> changes the semantics of the element, which is weird.

    Yes, it is, I agree. This is discussed here in the explainer. The one point that seems to kill the idea of creating a new element (e.g. <shadowroot>) is the backwards-compat problem. Until all browsers understand the new element, enclosed <style> and <script> elements will be exposed to the parent page, with potentially bad consequences.

Thanks again for the great points raised here!

That is exactly what I was envisioning - a small if (this.shadowRoot) block that just hooks things up if the shadowRoot already exists, and the actual construction code if not. That code works on both client and server (it is isomorphic), and the added code is minimal.

Personally I think it would be preferable that closed shadow roots can still be SSR-ed, I understand that .attachShadow({ mode: 'closed' }) is kinda weird when a shadow root is already attached so perhaps a way to close a shadow root after the fact would make more sense:

class MyComponent extends HTMLElement {
  #shadowRoot;
  constructor() {
    if (this.shadowRoot) {
      this.#shadowRoot = this.shadowRoot;
      this.#shadowRoot.close(); // Changes the shadow root from open to closed
    } else {
      this.#shadowRoot = this.attachShadow({ mode: 'closed' });
      // Initialize shadow root ...
    }
    // ....
  }
}

Clearly, the non-custom-element version results in many fewer bytes, and a less complex (i.e. more memory-efficient) DOM.

One suggestion I had on the original discourse thread was to use template instantiation so that data can be injected into a single template with even less duplication than current SSR approaches as they don't even need to duplicate the rendered DOM.

This suggestion would address @Rich-Harris concerns about duplication but depends on a very early proposal for template instantiation. Although as a plus the approach could still be used even with duplication because if template instantiation were added later it could be added on without changing the elements significantly e.g.:

<template id="world-clock-template" shadowroot="open">
    <style>
      span {
        font-variant: tabular-nums;
      }

      .seconds {
        font-size: 0.8em;
      }
    </style>

    <span class="hours">{{hours}}</span> :
    <span class="minutes">{{minutes}}</span> :
    <span class="seconds">{{seconds}}</span>
</template>

<template id="prerendered-2" shadowroot="open">
    <style>
      span {
        font-variant: tabular-nums;
      }

      .seconds {
        font-size: 0.8em;
      }
    </style>

    <span class="hours">07</span> :
    <span class="minutes">34</span> :
    <span class="seconds">56</span>
</template>

<template id="prerendered-3" shadowroot="open">
    <style>
      span {
        font-variant: tabular-nums;
      }

      .seconds {
        font-size: 0.8em;
      }
    </style>

    <span class="hours">20</span> :
    <span class="minutes">34</span> :
    <span class="seconds">56</span>
</template>

<p>The time in London is
  <world-clock shadowroot="#prerendered-1" timezone="GMT"></world-clock>
</p>

<p>The time in New York is
  <world-clock shadowroot="#prerendered-2" timezone="EDT"></world-clock>
</p>

<p>The time in Hong Kong is
  <world-clock shadowroot="#prerendered-3" timezone="HKT"></world-clock>
</p>

However with template instantiation this could just become:

<!-- With template instantiation -->

<template id="world-clock-template" shadowroot="open">
    <style>
      span {
        font-variant: tabular-nums;
      }

      .seconds {
        font-size: 0.8em;
      }
    </style>

    <span class="hours">{{hours}}</span> :
    <span class="minutes">{{minutes}}</span> :
    <span class="seconds">{{seconds}}</span>
</template>

<p>The time in London is
  <world-clock
    shadowroot="#world-clock-template"
    shadowrootdata='{ "hours": 12, "minutes": 34, "seconds": 56 }'
    timezone="GMT"
  ></world-clock>
</p>

<p>The time in New York is
  <world-clock
    shadowroot="#world-clock-template"
    shadowrootdata='{ "hours": 7, "minutes": 34, "seconds": 56 }'
    timezone="EDT"
  ></world-clock>
</p>

<p>The time in Hong Kong is
  <world-clock
    shadowroot="#world-clock-template"
    shadowrootdata='{ "hours": 20, "minutes": 34, "seconds": 56 }'
    timezone="HKT"
  ></world-clock>
</p>

I agree that problem b) is a problem that needs a solution. Perhaps the parser could detect exactly-duplicate <style> elements and condense them into a single CSSStyleSheet that gets added to adoptedStylesheets? That might be crazy. I do agree that styles need a solution. I don't think it needs to be solved for this declarative Shadow DOM solution to be useful as-is.

Can you elaborate? All browsers already avoid parsing duplicate inline stylesheets in Shadow DOM, as far as I'm aware.

From @sebmarkbage:

My initial reaction is very positive. It will take a long time until this ships in enough browsers that we'll actually consider using it.
We'd have to really shift the CSS strategy. Even then, I don't think we'd use shadow DOM as the primary encapsulation mechanism given the relative overhead on each level given that our components are so small and many. You might call them micro-components - you heard the buzzword here first.
However, for larger encapsulation of large third party or legacy parts of an app inside another one this would be very useful as an alternative to iframes.
I'd like to see this exist but probably won't make any immediate plans to adopt it.

I agree that problem b) is a problem that needs a solution. Perhaps the parser could detect exactly-duplicate <style> elements and condense them into a single CSSStyleSheet that gets added to adoptedStylesheets? That might be crazy. I do agree that styles need a solution. I don't think it needs to be solved for this declarative Shadow DOM solution to be useful as-is.

Can you elaborate? All browsers already avoid parsing duplicate inline stylesheets in Shadow DOM, as far as I'm aware.

Hmm - can you elaborate? I do know that duplicate <link rel=stylesheet> links will use a shared stylesheet. But I was not aware that exactly-duplicated inline <style> elements would end up with a single backing CSSStyleSheet object in memory. If that's true, then I would say the inline <style> element solution might be a good one here. Gzip will reduce the network overhead, and this stylesheet de-duplication will eliminate the memory overhead.

All browsers already avoid parsing duplicate inline stylesheets in Shadow DOM, as far as I'm aware.

How can it know they are dupes without parsing them?

How can it know they are dupes without parsing them?

Hashmap from text to parsed stylesheet representation effectively.

But I was not aware that exactly-duplicated inline <style> elements would end up with a single backing CSSStyleSheet object in memory. If that's true, then I would say the inline <style> element solution might be a good one here. Gzip will reduce the network overhead, and this stylesheet de-duplication will eliminate the memory overhead.

They do:

You don't get a pointer-identical CSSStyleSheet because that'd be observable, but they share StyleSheetContents (curious how all engines ended up choosing the same name for this), which means that they copy-on-write all the CSS rules and such.

(That's what happens with <link> as well, fwiw, you don't get a pointer-identical CSSStyleSheet either, as authors could mutate them independently)

@emilio thanks for the code links. With that in mind, it would seem that inline <style> elements might be the most straightforward solution to the styling problem. Perhaps we could even augment the element.innerHTMLIncludingShadowRoots API to also automatically inline <style> elements for any adoptedStylesheets it finds?

What text should those stylesheets have? The serialized representation of their CSS rules? Or the original text that was passed to replace() / replaceSync()?

Because for inline style you don't get serialized back the result of CSSOM mutations. It'd be weird if adoptedStyleSheets would do that.

I think it's important to be able to preserve the semantics of adopted stylesheets after round-tripping through shadow DOM serialization. Constructible StyleSheets are a shared CSSStyleSheet and that's observable as well.

@mfreed7 has seen this, but I've been vaguely proposing the idea of a new <style> type that would create a non-applied StyleSheet object with the constructed bit set, and a way to refer to it by idref from declarative shadow roots:

<html>

  <style type="adopted-css" id="style-one">
    /* ... */
  </style>

  <style type="adopted-css" id="style-two">
    /* ... */
  </style>

  <div>
    <template shadowroot="open" adopted-styles="style-one style-two">
      <!-- ... -->
    </template>
  </div>
</html>

Having a type other than text/css means that the styles won't apply to the document scope even in older browsers. It's also what allows it to have an adoptable CSSStyleSheet .sheet, which replace() works on as well.

The ids in adopted-styles would probably have to search in the global scope, or possibly in ancestor scopes, not just in the scope they're defined in (which may be nested in other declarative shadow roots).

How this plays with innerHTML/outerHTML is tricky, but if we have a new property/method for that (I think it should be a method so it can't be set and can take options), it could return the adopted-css in some determined place.

Note that these <style type="adopted-css"> elements would be very similar, maybe identical, to what's proposed for HTML Modules, in that they essentially create a Cascading Stylesheet Module.

Initial thought is that this is great for SSR/SSG (already noted as primary motivation) and also acknowledging this isn't necessarily meant to be hand-coded which again aligns with SSR.

In regards to Ionic, I think this would be a big win as it'd help us reduce/remove JS that converts a component's flat dom tree styled with scoped css, into a shadow root with encapsulated css once the JS kicks in.

Concerns:

  • Is it safe to say the solution would not have a noticeable flicker between the time the declarative SD is painted and the component's constructor is called?
  • Will there be performance issues when there's a large list of items, and each item also includes a many child elements with shadow dom? When stress testing this scenario you can see some noticeable differences between a solution using adopted stylesheets, and one inlining style tags into each shadow root (especially on low-end mobile devices which Ionic is targeted for).
  • It'd be pretty easy for devs to explode the size of their html in comparison to traditional html/css. Maybe I'm overthinking it, but what's largely unknown right now is how shared styles could even be possible, but if not, does that also add to the problems that adopted stylesheets should be solving?
  • Is it possible to avoid any additional JS checking the existence of a shadow root? Basically I'd like to avoid any new logic in the constructor if possible.
  • From what I can see, it seems a significant challenge is how styles are handled, especially around reusing styles. Curious if solving declarative adopted stylesheets should come first.

Overall I'm excited to see this discussion and absolutely can see how it'll benefit Ionic's use-case.

rniwa commented

@mfreed7 : Nice write up on https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md.

Could you get the new perf numbers after replacing template.content.cloneNode with template.content.importNode as well as just template.content? That would avoid adoption of the cloned nodes from the template's document into the destination document and cloning at all respectively. Otherwise, the most of runtime will be spent cloning & adopting nodes, not attaching a shadow root.

This would be a "parser-only" feature, and adding the shadowroot attribute to an existing <template> would have no effect.

Attribute injection, which is what this is responding to, means injecting them on the parser level. So the fact that this is a parse-only feature is not a protection against attribute injection.

Specifically, if a page has a <template> and you manage to get <script> inside it, right now that is safe. If you can also get this new attribute on the <template>, that is no longer safe...

I see you guys love reinventing the bike. This looks like a crappy, less declarative and non-standard version of XSLT 1.0 (which came out in 1999, latest version is 3.0). "Congrats" on wasting 20 years of web potential...

I feel a bit awkward leaving this comment here, but according to this tweet from stubbornella a proposal will emerge in the next week or so for declarative CSS scoping on the light DOM.

We currently do SSR+hydration with React and don't use Web Components. CSS scoping is all we'd even hope to gain from a declarative shadow DOM proposal. CSS scoping on the light DOM sounds like what we'd prefer. (I'd have to read the other proposal first to be sure, though, and Rich-Harris's remarks about duplicating styles might be a real concern there.)

In general, I think an adequate test of this proposal should demonstrate Web Component hydration: Running Web Components on the server side in a way that renders declarative shadow DOM without JS, and then running JS on the client side that converts all of this into programmatic Web Components. I suspect that if the current proposal were prototyped, it would result in the issues Rich called out, including reduced performance relative to just not using Web Components at all.

If you don't use shadow DOM, it's entirely possible that declarative shadow DOM is not for you. It's also entirely possible (and probably, IMO) that declarative shadow DOM will bring more people to use shadow DOM, but that's not a requirement for this proposal to be very, very useful to the people who already do.

In than case, I think my second point holds all the more strongly: someone should sit down and develop a working prototype of WC hydration that works great with excellent performance. (Ideally it would somehow perform better or at least no slower than comparable non-WC hydration solutions.)

To be clear, today we don't use shadow DOM partly because we do hydration, and shadow DOM doesn't work well with hydration. If WCs offered a superior hydration solution to non-WC hydration, then I think we would switch over.

Ideally it would somehow perform better or at least no slower than comparable non-WC hydration solutions.

I think it's worth emphasising that custom elements cannot, in the general case, rely on the declarative shadow DOM a) existing and b) matching the component author's expectations. You might be able to rely on that situation if you're a framework that controls the components and the page, like Stencil, but not if (for example) you're writing a distributable widget.

Because of that, I'd expect it to become common that component authors would simplify their lives by using approaches like this.shadowRoot.innerHTML = '...', which seems like it's more or less guaranteed to result in worse performance than existing (non-WC) hydration strategies (not to mention the aforementioned issues around losing transient state like focus).

CSS scoping is all we'd even hope to gain from a declarative shadow DOM proposal. CSS scoping on the light DOM sounds like what we'd prefer.

This is my position also. Regarding whether or not declarative shadow DOM is useful for people not already using it, @stubbornella made this comment the other day:

it’s not enough to build features for folks already using web components. We want to make sure new features are broadly considered useful by all kinds of different developers.

I think this is an essential point. Ideally, a new feature like this would grant new capabilities to web developers at large. I hope that feedback like mine β€” that this proposal wouldn't provide a reason to start using web components β€” is received as being constructive, as is my intent.

I'm maybe in the minory but I like ShadowDOM.

I like the encapsulation and the potential performance benefits possible today and in the future. Having a global style sheet space with tricks to scope component makes me feel uncomfortable. Lot's of performance and memory optimization are possible with ShadowDOM. Will we every get them I don't know but we at least we have a way out.

So naturally I'm positive about the proposal.

But I'm concern about the <template> use. Is that used for technical reason or is it really idiomatically correct? <template> tag are not visible and here you want them to be visible.

I would rather make shadowDOM available everywhere like:

(The use of '#shadowroot' is just to relate with what's in devtools today and the private class fields of js - the name is not important)

<host-element>
    <#shadowroot>
        <style>shadow styles</style>
        <h2>Shadow Content</h2>
        <slot></slot>
    </#shadowroot>
    <h2>Light content</h2>
</host-element>

or

<div>
    <#shadowroot>
        <style>shadow styles</style>
        <h2>Shadow Content</h2>
    </#shadowroot>
</div>

(Open by default)

+1 for @justinfagnani adoptedsheet inclusion because that's what you want with styles and not inline them everywhere

    <#shadowroot adopted-styles="style-one style-two">
        <h2>Shadow Content</h2>
    </#shadowroot>

You can still do

<template>
    <#shadowroot>
        <style>shadow styles</style>
        <h2>Shadow Content</h2>
        <slot></slot>
    </#shadowroot>
</template>

If you want to have a template, makes perfect sense.


Regarding SSR, I think this is what we want.

<my-element>
    <#shadowroot>
        <style>shadow styles</style>
        <h2>Shadow Content</h2>
    </#shadowroot>
</my-element>

As per the Custom Element Specs, until <my-element> is defined, the browser will render what is inside the Custom Element. Once the Custom Element is defined it will replace or upgrade it. You can do SSR, hydration, skeletons, placeholders, you name it.

It's native, IMO relatively simple to explain and understand and inline with existing semantics.
I would be very happy with this.

My 2 cents

@georges-gomes I think the use of a tag that associates the shadowdom has already been evaluated and there are interesting conclusions about this.

I like more the idea of declaring the fragment attribute on the template tag, with the aim of associating a persistent fragment to the <host> node and being indifferent to the use of shadowDom for native SSR declarations

<host>
     <template shadowroot="open">
            <h1>...</h1>
     </template >
    <template fragment>
           <h1>...</h1>
     </template>
     <h1>...</h1>
</host>
host.childFragments; // NodeList[#fragment];

This only materializes as an attribute what has already been mentioned in the issue #736 de @WebReflection.

The benefit of this is that multiple libraries attach their fragments in the same node eg:

<host>
      <template fragment="block-react">...</template>
      <template fragment="block-lit">...</template>
</host>
host.childFragments.get("block-react"); // #fragment

@emilio:

What text should those stylesheets have? The serialized representation of their CSS rules? Or the original text that was passed to replace() / replaceSync()?

Because for inline style you don't get serialized back the result of CSSOM mutations. It'd be weird if adoptedStyleSheets would do that.

I would say it should be the serialized representation of their CSS rules. Exactly akin to serializing the #shadowroot contents into <template shadowroot> declarative markup - in that case the content was imperatively created, but we still want to serialize it all as it exists fully-hydrated on the server. Yes, this is definitely a different mode of operation from normal <style> serialization. But perhaps this could be opt-in with the new serialization API?

By the way, I think this makes even more sense (subject to more bikeshedding):

var html = element.getInnerHTML({includeShadowRoots: true,
   convertAdoptedStylesheetsToInlineStyles: true});

With a function API, we can add options more easily, and make it clear what should happen.

@justinfagnani:

The ids in adopted-styles would probably have to search in the global scope, or possibly in ancestor scopes, not just in the scope they're defined in (which may be nested in other declarative shadow roots).

This is what I worry about with this proposal - no other web API has this type of cross-document reference structure, at least that I know of. See, e.g. this thread and this doc discussing options for how to reference one node from another. It seems that several rough conclusions have been reached: 1) cross-document references should be disallowed, and 2) an imperative API is preferred over an idref solution.

I'd really like to avoid tying this proposal for declarative Shadow DOM to an issue that doesn't (yet) have a good solution. Especially since, I think, an eventual style-ref proposal would still be backwards compatible with this declarative Shadow DOM proposal.

@AdamBradley:

Concerns:

  • Is it safe to say the solution would not have a noticeable flicker between the time the declarative SD is painted and the component's constructor is called?

I suppose this depends on how the component is written, but I wouldn't expect any "flicker". In the proposed example, the component constructor just detects the pre-existence of the #shadowroot and doesn't blow away and re-create any content. So I'm not sure where the flicker would come from.

  • Will there be performance issues when there's a large list of items, and each item also includes a many child elements with shadow dom? When stress testing this scenario you can see some noticeable differences between a solution using adopted stylesheets, and one inlining style tags into each shadow root (especially on low-end mobile devices which Ionic is targeted for).

I totally agree that we need to stress-test this. Several other people (e.g. @dfabulich) mentioned the need for a "real world" example. I'm planning to build such an example and test out the performance, but if anyone would like to volunteer a candidate set of basic generic components and a real-worldy tree of those components, I'd be appreciative.

  • It'd be pretty easy for devs to explode the size of their html in comparison to traditional html/css. Maybe I'm overthinking it, but what's largely unknown right now is how shared styles could even be possible, but if not, does that also add to the problems that adopted stylesheets should be solving?

I agree that we need a solution for shared styles - I've proposed some basic approaches above. But my hope (perhaps naive) is that the two problems, declarative Shadow DOM and declarative adopedStylesheets, can be solved roughly independently.

  • Is it possible to avoid any additional JS checking the existence of a shadow root? Basically I'd like to avoid any new logic in the constructor if possible.

I'm open to suggestions for how to accomplish this - it would seem that at least a basic !!this.shadowRoot check will be needed. Perhaps outerHTML could be used to just blow away the entire node in the constructor? That doesn't seem especially efficient.

  • From what I can see, it seems a significant challenge is how styles are handled, especially around reusing styles. Curious if solving declarative adopted stylesheets should come first.

I agree that these are a challenge. I'm hoping we don't need to solve that first, or that some of the above suggestions might be workable. But maybe not.

Overall I'm excited to see this discussion and absolutely can see how it'll benefit Ionic's use-case.

I'm really glad to hear that!

@rniwa:

Could you get the new perf numbers after replacing template.content.cloneNode with template.content.importNode as well as just template.content? That would avoid adoption of the cloned nodes from the template's document into the destination document and cloning at all respectively. Otherwise, the most of runtime will be spent cloning & adopting nodes, not attaching a shadow root.

Ahh very good catch. I'm sorry I missed that. Yes, I'll remove cloneNode from the baseline script-based element and re-run things. I'll check back here with the results when I've had a chance to run that.

@bzbarsky

Attribute injection, which is what this is responding to, means injecting them on the parser level. So the fact that this is a parse-only feature is not a protection against attribute injection.
Specifically, if a page has a <template> and you manage to get <script> inside it, right now that is safe. If you can also get this new attribute on the <template>, that is no longer safe...

I see - you're right. Attribute injection would allow the shadowroot attribute to get added to a <template> which would activate any child <script> elements. I'm not as familiar as I should be with the security aspects of this proposal. Can you elaborate on how one would also inject a <script> node inside the <template>? I.e. why is it more likely that this could happen inside <template> than outside? Otherwise, it seems easier to just skip the attribute injection and inject your <script> directly on the page somewhere, where it would run. Is it that existing sanitizers assume the insides of a <template> are safe?

@Rich-Harris:

I think it's worth emphasising that custom elements cannot, in the general case, rely on the declarative shadow DOM a) existing and b) matching the component author's expectations. You might be able to rely on that situation if you're a framework that controls the components and the page, like Stencil, but not if (for example) you're writing a distributable widget.

Can you elaborate on this concern? It would seem that if my SSR framework is running the code on the server to generate the SSR'd content, and then shipping that same code off to the client to run locally, the shadow content would always match (modulo bugs). I'm sure I'm missing something - what is it?

I think this is an essential point. Ideally, a new feature like this would grant new capabilities to web developers at large. I hope that feedback like mine β€” that this proposal wouldn't provide a reason to start using web components β€” is received as being constructive, as is my intent.

This is absolutely how I'm receiving your (and everyone else's) feedback. This has been a very helpful discussion.

@georges-gomes:

But I'm concern about the <template> use. Is that used for technical reason or is it really idiomatically correct? <template> tag are not visible and here you want them to be visible.

I would honestly prefer a new <shadowroot> tag myself. But I don't know how to get around the very real backwards compatibility problem. Before all browsers support this new feature, it is even worse than your concern about <template> being inert. The new <shadowroot> will be completely live on the owning page for browsers that don't understand that this new tag should represent an encapsulated shadow root. I'm open to suggestions here.

@UpperCod:

I like more the idea of declaring the fragment attribute on the template tag, with the aim of associating a persistent fragment to the node and being indifferent to the use of shadowDom for native SSR declarations

I hadn't seen that proposal - I'll take a look. In general, though, I'm trying to avoid gating this proposal on other in-progress APIs. I really want to make declarative Shadow DOM a reality.

Can you elaborate on this concern?

If I'm making <distributable-widget>, I have no control over how it's going to be consumed β€” whether it's going to be in an app that uses a WC SSR framework, or if it's going to be used in a document without declarative shadow DOM, or if it's going to be created programmatically. So at the very least, the constructor is going to need to accommodate both the case where declarative shadow DOM exists, and the case where it doesn't:

if (this.shadowRoot) {
  this.foo = this.shadowRoot.querySelector('.foo');
  // ...
} else {
  this.attachShadow({ mode: 'open' });
  this.foo = document.createElement('div');
  this.foo.className = 'foo';
  // ...
  this.shadowRoot.append(this.foo, ...);
}

But that lack of control extends to the content of the declarative shadow DOM. It's very easy to imagine scenarios in which there's a version mismatch (including version changes that aren't considered breaking, because changing a shadow DOM classname could be considered an internal implementation detail)...

<distributable-widget>
  <!-- code manually copied and pasted from some documentation on day 1 -->
  <template shadowroot="open">
    <div class="not-foo">...</div>
    ...
  </template>
</distributable-widget>

<!-- oops, only a major version specified! we get version 1.0.1, released on day 2 -->
<script type="module" src="https://unpkg.com/distributable-widget@1"></script>

...or something as simple as a misconfigured HTML minifier treats whitespace in a way that causes the DOM structure to be just different enough that this.p = element.childNodes[3] no longer works.

We might argue that these fall under 'user error', but regardless, we're introducing a source of brittleness that would be extremely difficult to debug.

But that lack of control extends to the content of the declarative shadow DOM. It's very easy to imagine scenarios in which there's a version mismatch (including version changes that aren't considered breaking, because changing a shadow DOM classname could be considered an internal implementation detail)...
...or something as simple as a misconfigured HTML minifier treats whitespace in a way that causes the DOM structure to be just different enough that this.p = element.childNodes[3] no longer works.

Thanks for the examples. The first definitely seems to me like user (or perhaps documentation?) error. I would hope that the documentation example would look more like this:

<distributable-widget>
  <!-- leave this empty - the component will populate it -->
</distributable-widget>
<script type="module" src="https://unpkg.com/distributable-widget@1"></script>

The HTML minifier example is interesting - I agree that this could cause problems. But wouldn't that be a "brittleness" in the component code? I know it is commonplace, but I always cringe when I see things like thirdButton = parent.childNodes[3]. That's brittle even without declarative Shadow DOM.

I do see the point, though. I'm open to suggestions on how to reduce this potential class of problems.

I would hope that the documentation example would look more like this:

<distributable-widget>
  <!-- leave this empty - the component will populate it -->
</distributable-widget>
<script type="module" src="https://unpkg.com/distributable-widget@1"></script>

But then you'd have to wait for the JS to load and run before seeing any content!

@mfreed7 thanks for taking the time to reply. The backward compatibility is a real problem and now I appreciate the proposal better. πŸ‘

But then you'd have to wait for the JS to load and run before seeing any content!

This is definitely true. I don't have a good solution to this problem - I suppose the only way might be to always blow away the shadow root content in the constructor and re-create it. Perhaps that's not so bad? It would get pixels on screen quickly with the declarative content, and then when hydration happens, rebuilding content is hopefully quick. This seems like a general issue - any time you have declarative content and a separate set of code to create it, they can get out of sync. Suggestions appreciated here.

thanks for taking the time to reply. The backward compatibility is a real problem and now I appreciate the proposal better. πŸ‘

I'm glad you agree with that being an issue. I really do prefer <shadowroot> but I don't see an easy way around the problem it poses.

This seems like a general issue - any time you have declarative content and a separate set of code to create it, they can get out of sync. Suggestions appreciated here.

Forgive me if you know all of this, but just to be clear, the core idea of React SSR+hydration is:

  1. compute a virtual DOM on the server side and render it as HTML
  2. compute a vDOM on the client side (this regenerates the component structure of the HTML but doesn't actually touch the DOM, which would be slower)
  3. quickly verify that the SSR HTML matches the client-side vDOM (e.g. by computing a hash code of the SSR vDOM and storing it in the HTML, then checking that hash code against the hash code of the CSR vDOM)
  4. a. If the vDOM matches the HTML, attach event listeners to the existing elements; now the page is interactive.
    b. If the vDOM mismatches the HTML, the client-side render will blow away the SSR HTML, which is just what you have to do in event of mismatch.

4b is not "supposed" to happen; the developer is supposed to provide the same state data on the server side and the client side, ensuring that the SSR vDOM matches the CSR vDOM. If the developer provides different data on the client and server, that's performance a bug that the developer can/should fix, but the result is just degraded performance, not a catastrophe. (It will often result in a flash of SSR content replaced by CSR content.)

With all that in mind, I'd like to reiterate Rich's earlier comment.

Rich pointed out that WCs can't make any assumptions about whether SSR HTML will exist or not.

β€’ By default, all existing WCs out in the wild will presumably do the slow/bad thing and just throw away the SSR'd HTML during the connected callback. That's slow because it throws away perfectly good SSR HTML, and it's also bad because it replaces the SSR HTML with new identical CSR HTML, erasing form state, focus state, etc.
β€’ WC authors could implement a very poor vDOM by just by assuming that the SSR HTML will match the CSR HTML and using querySelector to find elements and attach event handlers to them. But that's very fragile, because you might have mismatching HTML, and if you do, querySelector might not find the right element(s). The React approach is to require all components to participate in vDOM, supporting fast SSR verification, but to do that for WCs, it would require standardizing vDOM in the browser, which sounds like a Herculean task to me.

I interpreted your reply as "In that case, just don't SSR! You can leave the SSR HTML blank." I believe Rich understood your reply the same way I did. Rich pointed out that in that case we'd (of course) lose the benefits of SSR, "But then you'd have to wait for the JS to load and run before seeing any content!"

I think the overall moral of this story is that it will be very hard to beat non-WC hydration, which is why it would be prudent to prototype WC hydration sooner than later.

ergo commented

I would hope that the documentation example would look more like this:

<distributable-widget>
  <!-- leave this empty - the component will populate it -->
</distributable-widget>

But then you'd have to wait for the JS to load and run before seeing any content!

This is what I already do with server side responses in other languages like python or go - and it works great already. Web Components are the only tech that allows this that I'm aware of.

I thought the whole point of the proposal is to render the ShadowDOM contents.

If I'm making , I have no control over how it's going to be consumed β€” whether it's going to be in an app that uses a WC SSR framework, or if it's going to be used in a document without declarative shadow DOM, or if it's going to be created programmatically. So at the very least, the constructor is going to need to accommodate both the case where declarative shadow DOM exists, and the case where it doesn't:

@Rich-Harris With a natural extension to my suggestion above you could rely on templates coming from a specific url e.g.:

<world-time
  timezone="GMT+12"
  shadowroot="https://unpkg.com/world-time/template.html#default"
  shadowrootdata='{ "hour": 10, "minute": 20, "second": "30" }'
></world-time>
// world-time/component.js

const TEMPLATE_URL = new URL(
  './template.html#default',
  import.meta.url,
).href;

class WorldTimeComponent extends HTMLElement {
  constructor() {
    super();
    if (this.shadowRoot) {
      if (this.shadowRoot.templateUrl !== TEMPLATE_URL) {
        // DESTROY and replace shadow root
      }
    } else {
      // Create the shadow root
    }

    // Begin hydration, etc
  }
}
  1. quickly verify that the SSR HTML matches the client-side vDOM (e.g. by computing a hash code of the SSR vDOM and storing it in the HTML, then checking that hash code against the hash code of the CSR vDOM)

@dfabulich, thanks for the post. That made me understand the disconnect here. The point is that this proposal is not for a full SSR system. This proposal is for a necessary, low-level primitive that would be required for any Shadow-DOM-based SSR system. I'm not trying to solve your item 3 - that can be done a number of ways, and I'm not trying to propose a platform-standard way of diffing content. I suggested two trivial but non-optimal ways to do this: 1) always assume the SSR content matches and don't re-create anything, or 2) always assume it doesn't match and re-create it all. Clearly there are smarter, but more complex, solutions that can be provided by SSR frameworks. But no matter what the solution, there needs to be a declarative (no-JS) way to create a #shadowroot in the SSR content. That's what this proposal is all about.

inoas commented

@Rich-Harris what about NO-JS driven server side rendering such as Phoenix LiveView and what about simply using shadow doms to manually create components on one of the most used web plattforms: WordPress (not that I like it much).

In regards to loading styles, has it been addressed that a stylesheet should be loadable via HTTP2 but not be used to render the full DOM yet then be referenced by a shadow dom? Letting the browser parse through all the DOM and shadow-dom waiting for the html document to be loaded to just then load CSS for shadow doms seems wrong. If this has been addressed, please ignore.

I've updated the explainer with various things from this discussion:

  • Cleaned up the "Behavior" section, to hopefully get it a bit closer to spec language.
  • Added the new getInnerHTML() function to take the place of needing to opt-in to innerHTML serialization during attachShadow().
  • Added shadowroot_delegates_focus (and similar) attributes to the declarative <template> form.
  • Added a comment about CSS custom states and AOM IDL attributes from @justinfagnani.
  • Per @rniwa, I re-ran the performance section with the template.content.cloneNode() call removed. I also added another "script at the end" snippet, and used tachometer for better timing. TL/DR: the inline script approach is still ~5x slower, and the "script at the end" approach is 10% slower.
  • Changed the polyfill example to the @Rich-Harris Clock, which gives a better idea of how hydration might work. As mentioned above, this is just an illustration.

I have sent a Chromium Intent to Prototype for this feature, and it is available in basic form behind the DeclarativeShadowDOM runtime feature (or chrome://flags/#enable-experimental-web-platform-features). It is still very much in development, but if you find an interesting bug, please feel free to report it.

ergo commented

@inoas Non-JS server side rendering solutions like wordpress are already be able to render html with shadow dom tags. When corresponding JS is loaded the elements upgrade themselves just fine.
Like here: http://podswierkiem.org/o/1-razem-przeciw-przemocy-i-uzaleznieniom-2014 though this solution does not create any visible shadow DOM on load.

I've put together a more "complicated" set of examples, aimed at measuring the performance of SSR (using the proposed declarative Shadow DOM implementation in Chrome) as compared to CSR with inline styles and adoptedStylesheets. I've written it up here. As a TL/DR, here is the conclusion paragraph:

Between the two non-SSR examples, the version using adoptedStylesheets is about 4.5% faster than the version using inline styles. However, the SSR example is about 30% faster than both of the non-SSR examples, despite the HTML content being over 10x larger. Additionally, the SSR version does not suffer from a flash of partially-styled content.

Please take a look and let me know your thoughts.

Would you please rerun the performance tests with a low power Android device, e.g. a Moto G4, which is what WebPageTest uses?

My intuition is that 1MB of HTML can’t be good for low powered devices. If this technique makes desktop browsing faster but mobile browsing slower, it will only serve to entrench the mobile web’s performance problems.

Thanks for the comment. One quick note: the 1MB size was intended to make this page large enough to get good loading time statistics, not necessarily because it is a representative page size. This example page has 30*35 = 1050 components, which I suspect is somewhat large.

I will try to do some testing using WebPageTest, but I'm not sure exactly how to go about instrumenting the page so that the measurement times reported there are comparable. To my knowledge, there isn't a way to hook tachometer up to WPT to do apples-to-apples measurements in the same way. Any suggestions?

rniwa commented

I've put together a more "complicated" set of examples, aimed at measuring the performance of SSR (using the proposed declarative Shadow DOM implementation in Chrome) as compared to CSR with inline styles and adoptedStylesheets. I've written it up here. As a TL/DR, here is the conclusion paragraph:

It appears to me the most of overhead in non-SSR example comes from repeatedly executing innerHTML of the same markup in the constructor. If you modified each custom element you have so that it would parse the HTML just once & clone the nodes instead, I'm seeing ~30% improvement on my local machine:

class Carousel extends HTMLElement {
  static createShadowContent() {
    if (!this._shadowContentTemplate) {
      const template = document.createElement('template');
      template.innerHTML = `
       <div id=border>
         <button id=goleft></button>
         <slot id=contents>Empty Carousel</slot>
         <button id=goright></button>
       </div>
       <style id=inlinestyles>${myStyles}</style>
       `;
       this._shadowContentTemplate = template.content;
    }
    return document.importNode(this._shadowContentTemplate, true);
  }

  constructor() {
    super();

    const useInlineStyles = styleType === "inline";
    if (!useInlineStyles && styleType !== "adopted") {
      console.error("You must define styleType to be either 'inline' or 'adopted'");
      return;
    }

    if (!this.shadowRoot) {
      this.attachShadow({mode: 'open'});
      this.shadowRoot.append(Carousel.createShadowContent());
    }
rniwa commented

The point on the flush of contents also seems rather red-herring since you can always add some CSS rules to hide the content until all custom element definitions are loaded, or simply load custom element definitions as sync scripts at the very beginning of the document instead of at the end so that the pre-load scanner will go ahead & parse the HTML regardless.

rniwa commented

By the way, you should also serve this content over HTTP/2 and push the relevant scripts.

I expect there will be also interaction between the network latency, bandwidth, & device’s raw computing power.

For example, in a low latency, low bandwidth, high computing power situation, I’d expect non-SSR solution to be faster because it sends the smaller amount of bytes and the heavy lifting is done in the client side where we have ample computing power. You might be surprised how often fast phone like iPhone 7’s CPU sits idle waiting for network payload to arrive during a page load. Counterintuitively, doing more work in CPU sometimes result in the faster overall throughout because CPU doesn’t get throttled and more things at processed at once without costing much in power consumption.

On the other hand, in high latency, high bandwidth, low computing power situation, I’d expect SSR to be faster because the increase in the network payload doesn’t matter that much after gzip (although very low end CPU may suffer from uncompressing a lot of content).

All these scenarios need to be thoroughly tested before any concrete Perf claim can be made.

Web page load performance is one of the most complicated topic imaginable, and there are so many things that have unintuitive interactions with one another.

rniwa commented

For the purpose of quantifying the value of declarative shadow DOM, however, the most relevant measurements is about whether there is much benefit in creating a shadow root via markup vs. scripts as you’ve done in the previously experiment because that measurement focuses on script vs markup difference and gets rid of all other complicated issues with regards to SSR, which is probably outside the scope of this particular issue.

uasan commented

Hi, SSR for search bots.

But there is a problem, the tag A in the shadow DOM, the text content for A in the light DOM
(this real use cases).

The search bot, will understand such HTML correctly?

<host-element>
    <template shadowroot="open">
        <a href="/link">
          <slot name="link"></slot>
        </a>
    </template>
    <span slot="link">
      Link content
    </span>
</host-element>

For the purpose of quantifying the value of declarative shadow DOM, however, the most relevant measurements is about whether there is much benefit in creating a shadow root via markup vs. scripts as you’ve done in the previously experiment because that measurement focuses on script vs markup difference and gets rid of all other complicated issues with regards to SSR, which is probably outside the scope of this particular issue.

@rniwa thanks for all of the great feedback. I particularly agree with the above point - I should have stuck to my previous "pure" implementation of just declarative vs. imperative, as that is much less complicated. As you rightly pointed out, there are many ways to optimize for speed on various platforms, and my quick "real world" example is definitely not at all well-optimized. I suppose the reason I wanted to explore a more complicated example was just to find out if there were any major issues with the proposed approach, and start to see how it might be used in practice. Perhaps this example has done more to muddy those waters than it has helped!

It appears to me the most of overhead in non-SSR example comes from repeatedly executing innerHTML of the same markup in the constructor. If you modified each custom element you have so that it would parse the HTML just once & clone the nodes instead, I'm seeing ~30% improvement on my local machine.

Thanks for pointing out the innerHTML overhead, and doing your own testing. I'll take a look also, and see if I see the same thing. But it wouldn't surprise me, given the points above.

rniwa commented

By the way, we've been focusing a lot on the perf lately but there are other points to consider as well.

  1. Would declarative shadow DOM be still a useful primitive when we have declarative custom elements? If so, why? What concrete use cases would it address better than declarative custom elements?
  2. Are there benefits in having declarative shadow DOM in the browser beyond perf & not requiring scripts? If so, what are they? And what concrete use cases would that serve?
inoas commented

@rniwa on top of the speed differences and possible search engine improvements noted above by contributors much more in depth with the topic:

  1. Would custom elements be usable for scoping CSS (and CSR JS) to just one element and its children?
  • Declarative ShadowDOM can be generated and/or validated with XML/XSD/XSL tools on the server side and then diffs
  • With the advent of WASM in the mid-far future there could be options to use SSR and DOM APIs but not JS to make apps more save/reliable and speedy and build thin clients such as via the [Phoenix Live View](
    I have expected that too, but it can be engineerd very well to do SSR Phoenix Live View approach if say MorphDOM was standardized tech accessible via the WASM layer.
  • For privacy mindet people having unobstrusive JS (e.g. basic working websites / services, if not with all fancy options) becomes an option web developers CAN pick while still being able to scope CSS with Declarative ShadowDOM

@ergo

@inoas Non-JS server side rendering solutions like wordpress are already be able to render html with shadow dom tags. When corresponding JS is loaded the elements upgrade themselves just fine.
Like here: http://podswierkiem.org/o/1-razem-przeciw-przemocy-i-uzaleznieniom-2014 though this solution does not create any visible shadow DOM on load.

  • That takes HTTP2 Push to have the CSR JS available before DOM is loaded, right?
  • The client side shadow dom operation still has to run on the client either blocking the renderer or creating flash of unstyled content or similar, wrong?

Would declarative shadow DOM be still a useful primitive when we have declarative custom elements? If so, why? What concrete use cases would it address better than declarative custom elements?

Definitely yes, I think. People have been asking for style isolation for years, and adding a shadow is the simplest way to do so with the existing primitives in the platform. And style isolation is useful at a general "distinct parts of the page" level; requiring people to promote their element to a (single-use?) custom element to get this effect seems gratuitous. No matter how easy we make declarative custom elements, they'll always be additional steps and mental burden over just "put the contents of this element into a shadow for me", which is all you need and want for style isolation.

Avoiding additional unnecessary steps, even if they're objectively relatively simple, is the whole point of this entire exercise! A (complex) one-liner script that you copy-paste around is still, as we've seen, too much additional complexity for what people feel should be a simple operation.

Are there benefits in having declarative shadow DOM in the browser beyond perf & not requiring scripts? If so, what are they? And what concrete use cases would that serve?

Ergonomics is the biggest thing. If we want these things to be used by more authors, making them easy and simple to reach for, and more importantly, natural to reach for, rather than looking like an advanced hack for complicated use-cases only, is extremely important. An attribute or a wrapper element right at the point you want isolation to happen feels simple and more or less like normal HTML. A script that does some stuff and moves elements around does not.

Style isolation is highly requested, but my understanding is that a spec is forthcoming from @stubbornella for CSS scoping on the light DOM, which I think would scratch the "style isolation" itch much better than this proposal.

I predict that non-WC framework authors will enthusiastically adopt CSS scoping on the light DOM, and that the only users of Declarative Shadow DOM will be WC users who want SSR.

inoas commented

Style isolation is highly requested, but my understanding is that a spec is forthcoming from @stubbornella for CSS scoping on the light DOM, which I think would scratch the "style isolation" itch much better than this proposal.

I predict that non-WC framework authors will enthusiastically adopt CSS scoping on the light DOM, and that the only users of Declarative Shadow DOM will be WC users who want SSR.

As of now there is technology already that just needs another represenation that has its own benefits. Why reinvent the wheel. Also She announced that quite a whilte ago but I have yet to see it.

rniwa commented

Would declarative shadow DOM be still a useful primitive when we have declarative custom elements? If so, why? What concrete use cases would it address better than declarative custom elements?

Definitely yes, I think. People have been asking for style isolation for years, and adding a shadow is the simplest way to do so with the existing primitives in the platform. And style isolation is useful at a general "distinct parts of the page" level; requiring people to promote their element to a (single-use?) custom element to get this effect seems gratuitous. No matter how easy we make declarative custom elements, they'll always be additional steps and mental burden over just "put the contents of this element into a shadow for me", which is all you need and want for style isolation.

I hear you but we need a list of concrete use cases that support this argument.

Avoiding additional unnecessary steps, even if they're objectively relatively simple, is the whole point of this entire exercise! A (complex) one-liner script that you copy-paste around is still, as we've seen, too much additional complexity for what people feel should be a simple operation.

Are there benefits in having declarative shadow DOM in the browser beyond perf & not requiring scripts? If so, what are they? And what concrete use cases would that serve?

Ergonomics is the biggest thing. If we want these things to be used by more authors, making them easy and simple to reach for, and more importantly, natural to reach for, rather than looking like an advanced hack for complicated use-cases only, is extremely important. An attribute or a wrapper element right at the point you want isolation to happen feels simple and more or less like normal HTML. A script that does some stuff and moves elements around does not.

Ditto.

ionas commented

I hear you but we need a list of concrete use cases that support this argument.

  • Every Wordpress/Joomla/Drupla/Typo3 Plugin out there that features a contained frontend component
  • Every time CSS preprocessors are used to implement things like BEM http://getbem.com/introduction/
  • Every major componetized CSS framework such as Bootstrap. A whole classless approach came up just cause the lack of scoping, see Milligram https://milligram.io/ or MarxCSS
  • Every JS library that features standardized frontend components such as jQuery (yes, people still use that a lot) plugins or things like Ionic or Framework7

I would inverse the argument. Unless you have a self-contained SPA or very simple web page without logical components, containing styles and controlling if you inherit the surrounding context is a huge thing, aka it is useful for most of the web, not just some of it:

I hear you but we need a list of concrete non-use cases that don't support this argument.

I just wanted to quickly summarize my takeaways from the Web Components F2F discussion of declarative Shadow DOM on Monday:

  • I think there was a generally positive response.
  • it would seem that we need to make some allowances for allowing attachShadow() to blow away any existing declaratively-created shadow root. This is to avoid breaking existing components which assume attachShadow() will not throw, given that the component is hydrating the shadow root itself. This makes sense to me. There was a discussion about potentially making attachShadow() simply return the existing shadow root; however, this will likely also cause problems for components that then try to populate the shadow root and get collisions and duplicates.
  • there is a related, though separate, request to add an accessor to ElementInternals for the shadowRoot, including closed shadow roots. This would allow components to properly deal with closed declarative shadow roots. A new issue was opened for this. There has been some discussion of this point and the prior point on that issue.
  • there was another discussion about leaving the template element in, vs. removing it, once the declarative shadow root is attached. I believe we reached agreement here that the complexity of leaving it in outweighs the "weirdness" of the <template> node being "converted" to the #shadowroot.
  • there was another discussion about the lack of support for streaming (due to the shadow being attached upon the closing </template> tag). The implementers seemed to be on the same page that supporting streaming will be a significantly larger implementation effort.
  • There was a question/suggestion that getInnerHTML() should have an option to pass in closed shadow roots that are allowed to be serialized. Otherwise, there is no way to serialize closed shadow roots without violating encapsulation.

Please let me know if I missed anything important above. Based on the above, I believe my only action items are:

  • change attachShadow() so that it removes existing content if called while a declarative shadowroot exists.
  • add an option to getInnerHTML() to pass in closed shadow roots.
rniwa commented

As I elaborated in WICG/webcomponents#871 (comment), blowing away the exiting declarative shadow root could be the default behavior but we may also want to consider making it return the existing declarative shadow root if there is one.

rniwa commented

Now I realize the situation is a lot more complicated than that because of the sync parsing behavior in which case the custom element gets created before any of its children get parsed. In that scenario (non-upgrading case), you'd have to wait until all the children had been parsed for hydration.

Furthermore, other scripts on a page can attach a shadow root on the custom element before its children get parsed, or worse yet, anyone can access to template's content and have reference to those nodes, thereby breaking the encapsulation.

@rniwa, you are correct that a MutationObserver could be used to grab references to the contents of the <template shadowroot> before the closing tag is encountered, and therefore before the shadow root is attached. That would break encapsulation if the eventual shadowroot is closed. This is independent of whether the element is being created or upgraded, I think. Perhaps we could just disallow the .content accessor on the <template> element if it is a closed declarative shadow root?

On the sync parsing question for non-upgrading custom elements, I'm not sure how big of an issue this is in practice. For the SSR use case, none of the custom elements would be likely defined prior to the declarative content, so all custom elements would be upgraded later, after scripts are loaded. I don't know what the use case is that would load custom element definitions prior to sending declarative content, as that would seem to defeat the (No-JS) purpose. Perhaps I'm missing something, though.

rniwa commented

@rniwa, you are correct that a MutationObserver could be used to grab references to the contents of the <template shadowroot> before the closing tag is encountered, and therefore before the shadow root is attached. That would break encapsulation if the eventual shadowroot is closed.

This breaks encapsulation regardless of whether it's open or closed. This is one of those accidental leakage. Any code that's crawling across the DOM tree can bump into this.

This is independent of whether the element is being created or upgraded, I think. Perhaps we could just disallow the .content accessor on the <template> element if it is a closed declarative shadow root?

Right, this leakage is problematic in either case, and I agree disallowing content access is probably the most straight-forward fix here although it discloses the information that there is a declarative shadow tree in that case too.

On the sync parsing question for non-upgrading custom elements, I'm not sure how big of an issue this is in practice. For the SSR use case, none of the custom elements would be likely defined prior to the declarative content, so all custom elements would be upgraded later, after scripts are loaded.

I'm not sure about that. If custom element definitions are loaded as async scripts and the scripts are locally cached, they could be loaded & executed before the entire document finishes parsing.

rniwa commented

Either way, I don't think we can ignore the sync construction case given that exits, and we can't limit the use of declarative Shadow DOM to just SSR.

w.r.t. the encapsulation issue: If it's considered ok for the template to be removed automatically, why not prevent the template element from being appended at all if it has a shadowroot attribute?

rniwa commented

w.r.t. the encapsulation issue: If it's considered ok for the template to be removed automatically, why not prevent the template element from being appended at all if it has a shadowroot attribute?

Maybe. It does pose an interesting challenge / question about what happens to custom elements in those nodes but since template element's content are inert (e.g. custom element in it won't get upgraded), this is probably not observable.

Ok, there are two concerns being discussed here, on this thread and in issue 871. Since they're connected issues, I'm going to try to bring the discussion back to here for most of it.

The two issues as I see them are:

  1. The .content accessor on <template> can be used to break encapsulation and cause problems. Scripts and MutationObservers can be used to grab references to child nodes of the declarative <template>, among other things, while parsing is taking place.

Solution: for <template shadowroot> declarative templates, change the .content accessor to always return null. Since the <template> itself will only exist during parsing, and will be removed once the shadow root is attached and the <template> contents moved into the #shadowroot, this shouldn't cause any interop problems. It will just eliminate the possibility that script can come in and see the contents. This actually feels like a good improvement to the proposal overall - it should further reduce the possibility of corner case security issues.

  1. There is a race between declarative and imperative shadow dom. In the case where an async script will eventually call attachShadow(), and a declarative shadow root is defined in the HTML, either one could "win".

Here, I think it's important to recognize that this is a fundamental problem with almost any declarative solution. Since Shadow DOM only allows one shadow root, if there are two things trying to create one, there will be a race.

But importantly, I think this is ok. It can be intelligently handled by component authors, but even in the case of "DSD unaware" components, both cases seem ok:

a. In the "normal" case, the declarative shadow root "wins" the race. Then the component script executes and calls attachShadow(). Per my comment above, that call to attachShadow() removes the contents of the declaratively-created shadow root and returns it (empty) to the component. The component then populates it and moves on with life. This is not performance-ideal, because the work is done twice. But in the case of "DSD-unaware" components, we're just trying to not break things.

b. In the "backwards" case, the attachShadow() call from the component "wins" the race. Then the declarative content finishes parsing, and the parser tries to attach the declarative shadow root. In this case, that attachment will fail, and the declarative contents will be discarded. Here again, not performance-ideal, but everything keeps working correctly.

In the case of a "DSD aware" component, both situations can be properly detected. The 871 proposal addresses making sure components have access to existing #shadowroots, so that they can deal with both case a. and b. above in a performance-ideal way.

rniwa commented

b. In the "backwards" case, the attachShadow() call from the component "wins" the race. Then the declarative content finishes parsing, and the parser tries to attach the declarative shadow root. In this case, that attachment will fail, and the declarative contents will be discarded. Here again, not performance-ideal, but everything keeps working correctly.

I don't think this behavior is acceptable. There needs to be a way for a component to let the declarative content arrive into its shadow root. Maybe we need an option to attachShadow which indicates whether the declarative content could be inserted or not.

Also, this poses another problem. Since the component would never know when the children had finished parsing, there is no point in which the component is safe to attach a shadow root until your sibling or your ancestor's sibling starts appearing. Having to write that kind of code manually everywhere seems very fragile & developer hostile.

b. In the "backwards" case, the attachShadow() call from the component "wins" the race. Then the declarative content finishes parsing, and the parser tries to attach the declarative shadow root. In this case, that attachment will fail, and the declarative contents will be discarded. Here again, not performance-ideal, but everything keeps working correctly.

I don't think this behavior is acceptable. There needs to be a way for a component to let the declarative content arrive into its shadow root. Maybe we need an option to attachShadow which indicates whether the declarative content could be inserted or not.

Again here, there are two kinds of components we're talking about here. For "DSR unaware" components, all we're going for is "not broken". And I think we agree that this case is ok. For "DSR aware" components, which I think is what you're talking about when you request a new option to attachShadow(), the ElementInternals.shadowRoot can be used to not call attachShadow() and wait for the declarative content instead. Wouldn't that allow your use case?

Also, this poses another problem. Since the component would never know when the children had finished parsing, there is no point in which the component is safe to attach a shadow root until your sibling or your ancestor's sibling starts appearing. Having to write that kind of code manually everywhere seems very fragile & developer hostile.

So it sounds like you think we need to solve the parser finished / children changed problem. I agree (and it sounds like many developers do also) that we should solve that problem. However, I'm hoping we can avoid tying these two proposals together. There are myriad other problems that are also waiting on that solution, so this will just be one more such problem. Let's work on that one also! But let's not stall DSR while we wait.

I agree with @mfreed7 assessment of the problem, and the proposed solutions for DSD unaware components seems reasonable. I also agree that we should keep the parsing finished / children changed problem separate. As we progress defining the solutions for the cases of DSA aware components, things will look a lot more clear.

Also, this poses another problem. Since the component would never know when the children had finished parsing, there is no point in which the component is safe to attach a shadow root until your sibling or your ancestor's sibling starts appearing. Having to write that kind of code manually everywhere seems very fragile & developer hostile.

So it sounds like you think we need to solve the parser finished / children changed problem. I agree (and it sounds like many developers do also) that we should solve that problem. However, I'm hoping we can avoid tying these two proposals together. There are myriad other problems that are also waiting on that solution, so this will just be one more such problem. Let's work on that one also! But let's not stall DSR while we wait.

I think this issue is important given that, even if there is eventually a way to learn that the parser has finished parsing all descendants, you'll still need to add guards throughout your component to handle being in the 'waiting for a possible shadow root' state. Is there a way that declarative shadow root attachment could be pinned down to a specific point in time?

You could do this if declarative shadow root templates weren't allowed to be positioned as any random child of the component. For example, what if it was required to be the first child element? You could even get this to work in the constructor by making shadow root templates attach to their next sibling, rather than their parent.

@caridy thanks for your support here. I'm hoping we can move ahead with this proposal.

@bicknellr declarative shadow root attachment is defined here to occur when the </template> tag is parsed. If there is asynchronous script calling attachShadow() while parsing is also happening, then the timing (by definition, almost) cannot be pinned down. However, if we separately solve the "parser finished" problem, then the component will be able to know that the declarative shadow root is now attached. I don't think the positioning within the parent will help here, as the asynchronous script could arbitrarily execute even between the parent/host element being parsed and the declarative <template>. The same is true if we make the declarative shadow root attach to the next sibling. Fundamentally, if a component/SSR framework is sending declarative shadow root content and also calling attachShadow(), then the framework will need to handle this race. All I'm trying to ensure is that we've added the necessary low-level APIs here to make that straightforward.

I made a pull request to the DOM and HTML specs a few days ago for declarative Shadow DOM. I believe those PRs (plus some equivalent changes I made to the explainer) implement the "conclusions" from this issue thread. I believe these PRs cover the "put your scripts after the declarative SSR content" use case quite well, because there are no race conditions there. We will need a solution to the parser finished / children changed callback problem in order to support the "load async scripts and then load declarative content" use case (which allows a race) more completely. I believe almost any solution to that problem (which is already needed for several other good reasons) should work with this proposed declarative Shadow DOM solution. One possible solution, once declarative Shadow DOM is a thing, is to add a "declarative shadow DOM attached" callback. But again, I'd like to defer that discussion to the related thread.

For the reasons above, I'm hoping other implementers can take a look at the PRs and offer some reviews (and ideally support).

@mfreed7 Have you given any thought as to how streaming can work with declarative shadow DOM? I've used different techniques like in your proposal repo and the problem I always ran into is that if a big chunk of HTML is inside of templates then the user will not see them until the ending template tag. When using the common technique of having a single root component for your entire app, that means not painting until all HTML is parsed. That's unfortunate. That doesn't mean this doesn't still work well for smaller DOM trees.

Would it be possible to start inserting into the shadow root sooner than the closing template tag?


edit: Looks like you do discuss streaming implications here: https://github.com/mfreed7/declarative-shadow-dom#timing-attach-the-shadow-on-opening-or-closing-template-tag

@mfreed7 Have you given any thought as to how streaming can work with declarative shadow DOM? I've used different techniques like in your proposal repo and the problem I always ran into is that if a big chunk of HTML is inside of templates then the user will not see them until the ending template tag. When using the common technique of having a single root component for your entire app, that means not painting until all HTML is parsed. That's unfortunate. That doesn't mean this doesn't still work well for smaller DOM trees.

Would it be possible to start inserting into the shadow root sooner than the closing template tag?

edit: Looks like you do discuss streaming implications here: https://github.com/mfreed7/declarative-shadow-dom#timing-attach-the-shadow-on-opening-or-closing-template-tag

Thanks for the comment! Yes, as you mention, I did discuss this in the explainer, and I agree with you that streaming is not well supported by the current proposal. However, the reasons I settled on this side of the line are:

  • implementation complexity is significantly higher with streaming supported. This (complexity) was was the prior blocker to declarative Shadow DOM moving forward.
  • a streaming version of declarative Shadow DOM could always be added later, e.g. <template shadowroot=open streaming>, if desired. Given the complexity pushback before, I'd prefer to make this a "phase 2" feature, if possible.

What do you think?

I'm all in favor of incrementally adding features, thanks.

One suggestion came up in my TAG review related to the streaming concern. The idea was that perhaps we could specify declarative Shadow DOM in such a way that streaming support was implementation-optional. Support for attaching the shadow root at the closing </template> tag would be mandatory, but implementations could optionally decide to attach the shadow root at the opening <template shadowroot> tag, and parse child content directly into the #shadowroot. Because it is optional, implementations that did not want to tackle this more difficult streaming option could opt out. There would always need to be a developer opt-out, such as:

<template shadowroot="open" do-not-stream>
  <content>
</template>  <!-- Shadow root always attached here -->

We would also likely need a way to feature-detect support for streaming.

Thoughts?

I don't think we should add optional behavior of such significance. That's a pretty fundamental departure from the fairly interoperable parser and node tree model we have today.

I don't think we should add optional behavior of such significance. That's a pretty fundamental departure from the fairly interoperable parser and node tree model we have today.

I can definitely understand this position. Is there not any way to specify this clearly enough that the behavior is intuitive for developers, feature-detectable, and still allows an optional (mostly performance related) feature of streaming support? Or, to put it another way, how is this different from having another distinct feature ("streaming" declarative SD) which isn't implemented in all browsers?

I'm not sure, but to me that would not be an acceptable outcome either. If we really want streaming I'd rather we figure out how now and make a joint decision on whether to pursue that instead.

ionas commented

But didn't that discussion block the previous attempts and would slow down the creation and adoption of this feature in the first place?

rniwa commented

I'm pretty sure I've said this in the past but we consider the integration of closed shadow roots with custom elements as a must have. We can't support this proposal unless that is spec'ed out. Otherwise, this feature provides zero benefit for developers who are using closed shadow trees.

rniwa commented

But didn't that discussion block the previous attempts and would slow down the creation and adoption of this feature in the first place?

It most certainly would. So I think the answer to @plinss's comment in w3ctag/design-reviews#494 (comment) is that we cannot.

In particular, once script element is inserted into a shadow root which is connected, the script within will start executing synchronously. As a result, the process of inserting nodes into a shadow root is synchronously observable. If HTML is streamed into a shadow root, then the script execution will reveal the state of streaming for that precise reason. In addition, various events such as load event would fire for other elements as they get inserted into a connected shadow root so they too pose a significant script observable behavior difference.

I'm pretty sure I've said this in the past but we consider the integration of closed shadow roots with custom elements as a must have. We can't support this proposal unless that is spec'ed out. Otherwise, this feature provides zero benefit for developers who are using closed shadow trees.

@rniwa I believe we have defined a path forward for closed shadow roots support, mostly explained here: https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md#existing-declarative-shadow-roots. Do you think we need to have that part spec'd as well or can we just keep it as a separate feature associated to element internals?

rniwa commented

I'm pretty sure I've said this in the past but we consider the integration of closed shadow roots with custom elements as a must have. We can't support this proposal unless that is spec'ed out. Otherwise, this feature provides zero benefit for developers who are using closed shadow trees.

@rniwa I believe we have defined a path forward for closed shadow roots support, mostly explained here: https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md#existing-declarative-shadow-roots. Do you think we need to have that part spec'd as well or can we just keep it as a separate feature associated to element internals?

Yes, that part definitely needs to be spec'ed out. Recall that our position has been that people should be always using closed shadow roots, not open shadow roots. Without this part being figured out, all basis and arguments to have declarative shadow roots is basically moot.

Recall that our position has been that people should be always using closed shadow roots, not open shadow roots.

This quite surprises me. All popular documentation on shadow roots currently says that closed shadow roots are to be avoided.

Here's MDN's shadow DOM tutorial:
https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM

Note: As this blog post shows, it is actually fairly easy to work around closed shadow DOMs, and the hassle to completely hide them is often more than it's worth.

The rest of the MDN tutorial exclusively uses open shadow roots.

Google's documentation is even more severe:
https://developers.google.com/web/fundamentals/web-components/shadowdom#closed

Google's discussion of closed shadow roots is in the "Advanced topics" section, in a subsection called "Creating closed shadow roots (should avoid)" and the whole section is basically an argument about why closed shadow roots are bad.

Here's my summary of why you should never create web components with {mode: 'closed'}:

IIRC even some of Apple's publicly facing statements exclusively documented open mode, though I can't find it now. (It would have been in 2016…? WWDC 2016 didn't have a "What's new in Safari" talk that year, apparently.…)

Now that I go looking, your blog post from 2015 is the only documentation I've ever seen recommending closed shadow roots.

rniwa commented

Recall that our position has been that people should be always using closed shadow roots, not open shadow roots.

This quite surprises me. All popular documentation on shadow roots currently says that closed shadow roots are to be avoided.

We have been pretty consistent about our position for the past 6-8 years (e.g. see Maciej's www-style reply in 2014 for example, which references his reply in 2012).

IIRC even some of Apple's publicly facing statements exclusively documented open mode, though I can't find it now. (It would have been in 2016…? WWDC 2016 didn't have a "What's new in Safari" talk that year, apparently.…)

Now that I go looking, your blog post from 2015 is the only documentation I've ever seen recommending closed shadow roots.

I used closed mode by default and open mode for debugging purposes only in my blog post in 2015 if that's what you're referring to.

I would be very curious to see some developer-oriented advocacy around closed roots, e.g. as a published article on the webkit blog. IMO, the de facto default is open because all of the documentation says "open is great, closed buys you nothing"

I'm pretty sure I've said this in the past but we consider the integration of closed shadow roots with custom elements as a must have. We can't support this proposal unless that is spec'ed out. Otherwise, this feature provides zero benefit for developers who are using closed shadow trees.

@rniwa I believe we have defined a path forward for closed shadow roots support, mostly explained here: https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md#existing-declarative-shadow-roots. Do you think we need to have that part spec'd as well or can we just keep it as a separate feature associated to element internals?

Yes, that part definitely needs to be spec'ed out. Recall that our position has been that people should be always using closed shadow roots, not open shadow roots. Without this part being figured out, all basis and arguments to have declarative shadow roots is basically moot.

Thanks for the comments, @rniwa. In the interest of trying to move this feature forward, I'd like to get a few clarifications:

  • While we disagree about the benefits and issues associated with streaming support here, I think we've reached somewhat of a consensus that streaming support can be added later, with something like <template shadowroot=open streaming>. So let's table that part of the discussion.

  • As @caridy points out, we've discussed closed shadow root support in the explainer and in Issue 871. You are saying that without resolving Issue 871, you cannot support this declarative Shadow DOM proposal. I understand your concern, and I'd like to solve it. But is the inverse of your statement true? I.e. if we resolve Issue 871, could you then support declarative Shadow DOM?

I've just proposed a fix for Issue 871, which I will commit to implementing in Chromium immediately, if agreed upon. Please take a look there, and let's get that issue resolved. And then I'm hoping you'll be supportive of this proposal.

rniwa commented

Yes, that part definitely needs to be spec'ed out. Recall that our position has been that people should be always using closed shadow roots, not open shadow roots. Without this part being figured out, all basis and arguments to have declarative shadow roots is basically moot.

Thanks for the comments, @rniwa. In the interest of trying to move this feature forward, I'd like to get a few clarifications:

  • While we disagree about the benefits and issues associated with streaming support here, I think we've reached somewhat of a consensus that streaming support can be added later, with something like <template shadowroot=open streaming>.

I'm not sure. I certainly don't want to support two different variants of this feature.

  • As @caridy points out, we've discussed closed shadow root support in the explainer and in Issue 871. You are saying that without resolving Issue 871, you cannot support this declarative Shadow DOM proposal. I understand your concern, and I'd like to solve it. But is the inverse of your statement true? I.e. if we resolve Issue 871, could you then support declarative Shadow DOM?

In my view, WICG/webcomponents#871 is the biggest blocker. I can't definitively say we'd support this proposal yet because I'd like to confirm the performance benefit claims made in the favor of this feature on our end. I'd try to do that sometime soon.

I've just proposed a fix for Issue 871, which I will commit to implementing in Chromium immediately, if agreed upon. Please take a look there, and let's get that issue resolved. And then I'm hoping you'll be supportive of this proposal.

I have to think through the use cases and circle back with my colleagues but on surface that does look like a reasonable solution to me, and it does indeed remove the biggest blocker of this proposal in my view. Again, I'd like to confirm the performance benefit claims if there is any on my end and need to circle back with some of my colleagues who are more skeptical of this feature in general to definitely say whether can support this feature or not.