mozilla / standards-positions

https://mozilla.github.io/standards-positions/
Mozilla Public License 2.0
651 stars 72 forks source link

Constructable Stylesheets #103

Closed rakina closed 5 years ago

rakina commented 6 years ago

Request for Mozilla Position on an Emerging Web Specification

Other information

There might be small API changes due to current open issues but the basic idea should stay the same, that is to allow creating CSSStyleSheet objects and use them in DocumentOrShadowRoot.adoptedStyleSheets. See also explainer.

marcoscaceres commented 6 years ago

I'm not an expert in this area, but I'm wondering how this relates to "CSS in JS". There are a ton of libraries that do "CSS in JS", then output a component-specific stylesheet (via webpack, babel, or whatever).

How does this proposal play with those?

rakina commented 6 years ago

Hmm, I'm not sure if I'm getting this right, but looks like most CSS-in-JS doesn't work with shadow DOM. The basic idea seems to be similar, though generated CSS-in-JS will be creating a real style element put into the document so it can't cross through shadow boundaries.

Also, I don't think we can create complex selectors with CSS-in-JS (probably I'm wrong)? With constructable stylesheets, we can get the full feature of CSS.

marcoscaceres commented 6 years ago

@rakina would it be worth me bringing in some "CSS in JS" folks to comment? I know a few that would be really interested in this, and might have some suggestions about what they need. However, happy to hold off too if you think you are not there yet with the proposal.

rakina commented 6 years ago

@marcoscaceres Oh, I'd be happy to have more people giving suggestions! Yes please, I think that would be really helpful. Thanks!

Also FYI, I intend to bring this topic for discussion in the CSSWG session at TPAC this year (https://wiki.csswg.org/planning/tpac-2018)

marcoscaceres commented 6 years ago

Ok, cool. Let me ping a few devs. Hopefully they will be willing to comment!

rakina commented 6 years ago

Another thing that might be related to Constructable Stylesheets & CSS-in-JS: CSS Modules https://github.com/w3c/webcomponents/issues/759

dbaron commented 5 years ago

I think the basic idea here is reasonable, and while it's not clear to me how we'd prioritize it, and I also suspect the specification needs a good bit of further work to be defined precisely enough and integrate properly with other specifications, it seems like a reasonable addition to the platform, so I'd be inclined to mark it as worth prototyping.

Does that seem reasonable to the others here?

heycam commented 5 years ago

I think the general idea seems OK. There are probably other ways you could come up with to solve the problem of avoiding duplication of style sheets between different instances of a Web Component, but an API to create individual, sharable style sheets to insert into different shadow trees is a reasonable way to do it.

dbaron commented 5 years ago

I've heard secondhand that webkit folks are opposed to this; I'm not entirely sure where that came from, but curious if @hober knows.

emilio commented 5 years ago

Most of the opposition was to the "introducing new cascade origin" bit which is gone from the spec IIRC.

emilio commented 5 years ago

Though not sure if they have any plans or official stance on it. @rniwa maybe knows?

rniwa commented 5 years ago

We had concerns about the specific shape of the API; e.g. adoptedStyleSheets is a very awkward name and its semantics (FrozenArray) doesn't seem to fit the need basic need of authors since each subclass of a custom element is likely adding its own stylesheet in which case having to duplicate & append your own stylesheet seems like way less intuitive than calling add on StyleSheet. But we think the general idea is good.

rniwa commented 5 years ago

Note that the cascading order issue is somewhat orthogonal; that is about "lightweight" mechanism to add styles to a custom element without a shadow tree.

emilio commented 5 years ago

Ah, indeed, sorry I misremembered. Thanks @rniwa :)

Lonniebiz commented 5 years ago

The reason I want Constructable Stylesheet support in Firefox (ASAP) is here.

Basically, it eliminates a lot of redundancy that comes with embedding <style></style> directly into the shadowDOM. If a custom element is used 100 times on a web page, instead of each shadowDOM node containing a redundant copy of that embedded CSS style, it instead references what would normally be an excessive redundancy.

This article has a nice table that shows what's currently missing from Firefox in regards to web components. Also, I found this video.

emilio commented 5 years ago

I don't expect constructible stylesheets to be a performance improvement over <link rel="stylesheet"> on a shadow tree, as @rniwa mentioned in https://github.com/w3c/webcomponents/issues/800. It's mostly a convenience thing, as far as I understand it.

Lonniebiz commented 5 years ago

It's mostly a convenience thing, as far as I understand it.

Indeed, it is. It will be nice to set the stylesheet using a javascript reference like this.

blikblum commented 5 years ago

@emilio

I don't expect constructible stylesheets to be a performance improvement over on a shadow tree

In this test, the same stylesheet (bootstrap.css) is loaded twice (one due to the link in head and other due the link in a custom element shadow tree).

This behavior occurs on Firefox but is not observed on chrome (where the stylesheet is loaded once)

If a stylesheet is linked only from shadow tree (foundation.css) its loaded once in both browsers

I identified the css load by using the Network devtool panel. Not sure if it means an extra CSS parse.

So:

PS: for a cleaner way to observe the behavior access the codepen debug view

emilio commented 5 years ago

In this test, the same stylesheet (bootstrap.css) is loaded twice (one due to the link in head and other due the link in a custom element shadow tree).

I don't see any link in the html section of the codepen, but I guess you mean the one in the <head> of the outer page, which is in another document (the pen is loaded in an <iframe>, and a sandboxed iframe at that).

If so, it is expected to fetch the stylesheet twice, once for the outer document, and once for the inner one. But once the stylesheet is loaded in one of the documents, the you don't see other loads, as expected. I cannot access the debug view:

This debug view expired. If this is your Pen or you are PRO, log in to view it in debug mode.

But I assume it's similar.

In any case the behavior of Firefox here is the same as in Chrome, as far as I can tell from the network panels:

Firefox Nightly

Firefox

Chrome

chrome-loads

Both have two loads of bootstrap.css (and thus, I expect, two reparses of the sheet). But as I said there are two different documents involved, which can't access each other via Javascript at all, so it's not something you could solve with constructable stylesheets even if the two documents wanted to cooperate.

So I don't really know what you're referring to, or how is it relevant to this issue, but it might (probably) be that I misunderstood the test-case. Mind elaborating?

Sharing parsed stylesheets across <iframe>s is theoretically doable, I guess, but there are a lot of edge cases (documents that have different CSPs, addons that may want to stop the load from one document but not other, etc...) that makes it kind of complicated, and thus I suspect no browser does it.

blikblum commented 5 years ago

Hi @emilio thanks for your feedback.

The default codepen interface adds lots of noise. The debug view shows only the pen html but is only accessible by owners (needs to fork the pen to access it).

I've put the cleaned test page here.

At the time i created this test, and until yesterday, the bootstrap.css was requested at page load and at first time a custom element with a link or import was added to document. I swear. This was the reason i stopped using shadow dom

Today, retesting, bootstrap.css is still requested at page load but not at custom element addition matching the chrome behavior (and probably moving away the performance concern)

Sorry for the false alert

blikblum commented 5 years ago

Regarding potential benefits of Constructable Stylesheets over using link or import i see two:

emilio commented 5 years ago

Avoid http request for each css file. When using libraries like LitElement (that uses adoptedStyleSheets when available) and bundlers like webpack, the styles are added to the bundle. This can have great impact when having a lot of components or many separated css files

How is this helped by constructable stylesheets? I'm confused. Do people just create a giant CSS file, and then chunk it into multiple CSSStyleSheets? That doesn't sound particularly great either... Otherwise I don't know how would it help.

Avoid flash-of-unstyled-content (FOUC) . This can be seen here. Look for "How to include CSS into Shadow DOM"

You can already do this without constructable stylesheets, fwiw. Code below untested, but something on those lines should do:

function sheetLoaded(url) {
  return new Promise((resolve, reject) => {
    let link = document.createElement("link");
    link.rel = "alternate stylesheet";
    link.title = "dummy";
    link.href = url;
    link.onload = function() { resolve(); link.remove(); };
    link.onerror = function() { reject(); link.remove(); };
    document.head.appendChild(link);
  });
}

// ...

await sheetLoaded("foo");
// load shadow trees with <link rel="stylesheet" href="foo">

Though I agree constructable stylesheets provide a less hacky way of doing that.

emilio commented 5 years ago

At the time i created this test, and until yesterday, the bootstrap.css was requested at page load and at first time a custom element with a link or import was added to document. I swear. This was the reason i stopped using shadow dom.

FWIW, I can reproduce the issue you mention, only if I've done something like opening the devtools inspector before. And this is because devtools pokes at the document stylesheets using CSSOM, and thus we determine we can't reuse the cached stylesheet, since it may be different.

I'll track it down and see if it's avoidable, but if you see something like that, instead of avoiding the feature (or at least on top avoiding the feature) reporting a bug would be useful.

blikblum commented 5 years ago

How is this helped by constructable stylesheets? I'm confused. Do people just create a giant CSS file, and then chunk it into multiple CSSStyleSheets? That doesn't sound particularly great either... Otherwise I don't know how would it help.

Take as example how styling works in LitElement.

It provides a tagged template literal css that converts a string containing css into a CSSStyleSheet instance.

When element is instantiated, before rendering occurs, it sets the styles defined by css to adoptedStyleSheets.

This way, before rendering occurs the stylesheets are already loaded and parsed, so no FOUC.

Also since the styles are defined as Javascript (tagged template literals) they can be bundled together by, e.g., webpack.

Example:

//sharedstyles.js
export const baseStyles = css`h1 {color: red};`
export const tableStyles = css`table {color: blue};`

//my-element.js
import { LitElement } from 'lit-element' 
import { baseStyles, tableStyles } from 'sharedstyles'

class MyElement extends LitElement {
  static get styles() {
    return [ css`:host { display: block; }`, baseStyles, tableStyles];
  }
}

Here's an actual project with this kind of setup: https://github.com/CitizensFoundation/open-active-voting/tree/master/public/src/components . The styles lives in the *-styles.js files

blikblum commented 5 years ago

I'll track it down and see if it's avoidable, but if you see something like that, instead of avoiding the feature (or at least on top avoiding the feature) reporting a bug would be useful.

At the time i was not sure if it was a bug and there's the FOUC issue. Also, since my projects depends on Bootstrap which does not plays well with shadow dom i opted to disable it.

But glad to know you are receptive to bug reports

emilio commented 5 years ago

Take as example how styling works in LitElement. [...] Here's an actual project with this kind of setup: https://github.com/CitizensFoundation/open-active-voting/tree/master/public/src/components . The styles lives in the *-styles.js files

Sure, but that doesn't avoid any http request. As far as I can tell what happens is:

It's a bit unfortunate that they chose that implementation strategy. The alternative to adoptedStyleSheets causes Safari and Firefox to reparse the stylesheet and keep a separate version of the stylesheet in memory for every instance of the component. Using a <link> with a blob URL would probably have the same performance characteristics as adoptedStyleSheets. cc @justinfagnani.

blikblum commented 5 years ago

Sure, but that doesn't avoid any http request. As far as I can tell what happens is:

Say we have two version of a webpack app:

//sharedstyles.js
export const baseStyles = css`h1 {color: red};`
export const tableStyles = css`table {color: blue};`

//my-element.js
import { LitElement } from 'lit-element' 
import { baseStyles, tableStyles } from 'sharedstyles'

class MyElement extends LitElement {
  static get styles() {
    return [ css`:host { display: block; }`, baseStyles, tableStyles];
  }
}

In the above using styles with css tagged template will be one request:

bundle.js

Now the same app using link to style:

//my-element.js
import { LitElement, html } from 'lit-element' 

class MyElement extends LitElement {
  render() {
      return html`
         <link rel="stylesheet" href="base-styles.css">
         <link rel="stylesheet" href="table-styles.css">
         <link rel="stylesheet" href="my-element.css">
      `
  }
}

You have four requests:

bundle.js base-styles.css table-styles.css my-element.css

So, the number of requests of the app will increase together with the number of styled components.

The alternative to adoptedStyleSheets causes Safari and Firefox to reparse the stylesheet and keep a separate version of the stylesheet in memory for every instance of the component

You are right about this. This is one of the reasons i use web components with shadow dom disabled and still use a global stylesheet with sass (it works fine BTW).

Firefox is my main browser and fully supports it. I ended here to know status of adoptedStyleSheets so i can embrace shadow dom

Lonniebiz commented 5 years ago

For me, the most important aspect of this, is the ability for the shadowDOM to acquire its style(s) via javascript variables, and not by parsing html "style or link" tags.

Take a look at this example. My favorite line in that example is this one: this.shadowRoot.adoptedStyleSheets = [styles];

"styles", is a javascript variable!

So, you can download or dynamically generate the style sheet once, and then have every web-component-instance consume that same single memory-reference.

If I have 1000 instances of a web component on a single page. Those instances should NOT do any of the following: 1) Those instances should NOT "download", "parse", or "dynamically generate" the same styles multiple times. 2) Those instances should NOT contain style or link elements; HTML doesn't have to contain any elements about style. To do so is inefficient and semantically irrelevant.

CSS is used to style HTML. And the most efficient way to associate a particular CSS style to multiple DOM Element Nodes, is to have each of those nodes have memory references pointing to that single CSS Style instance. In other words, via a javascript variable.

blikblum commented 5 years ago

It's a bit unfortunate that they chose that implementation strategy. The alternative to adoptedStyleSheets causes Safari and Firefox to reparse the stylesheet and keep a separate version of the stylesheet in memory for every instance of the component. Using a with a blob URL would probably have the same performance characteristics as adoptedStyleSheets.

Added an issue in lit-element https://github.com/Polymer/lit-element/issues/761 With a PR: https://github.com/Polymer/lit-element/pull/762

Lonniebiz commented 5 years ago

Why is this bug "closed"?

If I open up a console in Firefox 68.0.2, and type: let sheet = new CSSStyleSheet();

I get this error: TypeError: Illegal constructor.

As I explained here, the ability to reference a stylesheets using a javascirpt variable is fundamental and essential for performance sake when styling numerous instances of a shadowRoot-driven custom element.

This stuff works great in Chromium. Has Firefox decided not to implement this?

dbaron commented 5 years ago

This repository covers positions on emerging web standards in the discussions of standards development, not implementation in Firefox, which is covered by bug 1520690

Lonniebiz commented 5 years ago

@dbaron Thank you for explaining and linking to the bug above.