Re: Media Query Variables

On Sat, 14 Sep 2013 23:22:06 +0200, Kornel Lesiński <kornel@geekhood.net>  
wrote:

> On Sat, 14 Sep 2013 19:17:56 +0100, Simon Pieters <simonp@opera.com>  
> wrote:
>
>> I don't understand what you mean by "JS applications"
>
> I mean pages that have no useful markup in <body> and create entire  
> markup with JavaScript and data fetched from AJAX API or  
> localStorage/WebSQL/IndexedDB.

OK.

> These websites are already taking heavy performance hit on first load,  
> can't be accelerated with preload scanner, and have CSS loaded long  
> before final HTML markup is created, for example: http://m.flickr.com

That page appears to be having the main image in the markup, so probably  
not a good example.

>> OK, good. I think that is acceptable also. It doesn't necessarily need  
>> to be a syntax error, it could be parsed fine but just have no effect,  
>> either way.
>
> I'm worried that even if variables work in <style> authors will try to  
> use them in external stylesheets,

Yeah. <meta> doesn't have that problem.

> and it may be impossible to add support for variables in external  
> stylesheets later.

If there ends up being content that use it in external stylesheets and  
rely on it *not* working, and we want to add support for it, we would have  
to choose a new name or new syntax. Which would be ugly, but not  
impossible.

>>> This solution enables non-blocking loading. It just doesn't force it.
>>
>> Behavior should be well-defined.
>
> For ease of implementation I'm suggesting following simple algorithm  
> that IMHO makes behavior well-defined:

It's not clear to me whether you are proposing the delaying to be optional  
or not. We should choose one or the other and require it, or not support  
declaring variables externally.

> 1. If a variable is not defined then var(name) is false (does not match).
> 2. If the variable is defined at any point later, then media queries  
> using that variable must be re-evaluated and may start matching.
>
> The same way browser keeps track of media queries that use min-width:  
> and re-evaluates them when viewport size changes, the browser would keep  
> track of media queries using variables and re-evaluate them whenever  
> variables become defined.
>
> Case #1:
>
> <link rel="stylesheet"...>
> <picture>
> <source media="var(foo)"...>
> <source media="var(bar)"...>
> </picture>
>
> <source> selection algorithm is run in preload scanner and no media  
> matches, no image loaded.

Note again that the preload scanner is *not* involved in this case. It  
only kicks in when you have a <script src> that blocks the parser.

> When browser encounters @var-media foo:... and @var-media bar: it needs  
> to re-run source selection and check which one matches now and load a  
> matching source.
>
> For performance the browser should re-evaluate <source> with variables  
> after all @var-media rules in a stylesheet are parsed (basically always  
> schedule re-evaluation of <picture> sources on next tick instead of  
> doing it synchronously).
>
> e.g. if external stylesheet has:
>
> @var-media bar: all;
> @var-media foo: all;
>
> it would be wasteful if parsing of @var-media synchronously triggered  
> re-evaluation of <picture> and caused second source to immediately match  
> just to switch source on the next line of CSS. That is performance  
> optimization, does not change final result of the algorithm, just avoids  
> having unnecessary intermediate states.
>
>
> Case #2:
>
> <style>
> @var-media foo: not all;
> @var-media bar: all;
> </style>
> <picture>
> <source media="var(foo)" ...>
> <source media="var(bar)" ...>
> </picture>
>
> Preload scanner knows to download second <source> immediately.
>
>
> Case #3:
>
> <style>
> @var-media foo: not all;
> @var-media bar: all;
> </style>
> <link rel="stylesheet"...>
> <picture>
> <source media="var(foo)" ...>
> <source media="var(bar)" ...>
> </picture>
>
> Preload scanner knows to download second <source> immediately. If  
> external stylesheet redefines foo or bar, picture selection algorithm  
> has to be re-run and another image may be loaded instead.
>
> If inline <style> and external stylesheets define variables in  
> conflicting ways and CSS is not cached (or UA loads it async anyway),  
> then race condition could cause a wasted download, but the same, correct  
> image would be displayed eventually.
>
>
> Case #4:
>
> <picture>
> <source media="var(foo)" ...>
> <source media="var(bar)" ...>
> </picture>
>
> No image is ever loaded.
>
>
> Case #5:
>
> <link rel="stylesheet"...>
> <picture>
> <source media="var(foo)" ...>
> <source ...>
> </picture>
>
> Selection algorithm in preload scanner will match the second source and  
> start loading it. If stylesheet loads later and defines var(foo), then  
> source selection algorithm will be re-run and if var(foo) matches then  
> first source will be loaded and displayed instead.

This is the case I think we shouldn't allow. I think both starting  
downloading the wrong image and waiting for the CSS to be loaded are bad.

> If stylesheet was cached and var(foo) that matches was defined when  
> preload scanner reached the <picture> then the first source will be  
> loaded and displayed (same visible result like in case of delayed  
> stylesheet load, but avoiding wasted download).
>
>
> So I think behavior is well-defined. Case #2 works with preload scanner  
> perfectly. Case #1 is what some authors may prefer, and works reasonably  
> well (with expected latency).
>
> Cases #3, #4, #5 are bad performance practices/authoring errors, but  
> still result in well-defined predictable behavior.
>
> To avoid performance hit of case #5 I suggest deferring source selection  
> until CSS is loaded *if* the source selection algorithm encounters a  
> media query with an undefined variable.

It doesn't really avoid the performance hit, but it avoids a wasted  
request.

> Here's a draft of simplified <picture> source selection algorithm. I'm  
> not going to try to write the algorithm in Hixie style. Here's JS-ish  
> pseudocode instead:
>
>
> function sourceSelection(picture) {
>  var mq;
>  for(var i=0; i < picture.childNodes.length; i++) {
>   var child = picture.childNodes[i];
>   if (child.tagName != 'SOURCE' || child.namespaceURI !=  
> xhtml_namespace) continue;
>
>   if (child.getAttribute('media')) {
>    var mq = matchMedia(child.getAttribute('media'));
>    mq.addEventListener(sourceSelection); // re-run selection algorithm  
> whenever MQ changes
>
>    // marked line, see below
>
>    if (!mq.matches) continue;
>   }
>
>   if (child.getAttribute('type')) {
>    if (!isSupportedMimeType(child.getAttribute('type'))) continue;
>   }

So this skips past <source>s that have an unsupported type="", but the  
algorithm doesn't care if the returned image is unsupported. I think the  
type check should be removed since the main purpose here is to switch on  
MQ, not on type. (For <video> it's the opposite.)

>   return child;
>  }
>  return null;
> }
>
> In short: choose the first <source> child that has matching media and  
> supported type (missing attributes pass the test). If no matching  
> <source> is found then it's a broken image (like <img> without src).
>
>
> And to prevent potential wasted download in Case #5 outlined above,  
> replace "// marked line, see below" with:
>
>    if (!CSSisLoaded && mq.cannotFullyEvaluateYet) {        
>     addEventListener('CSSisLoaded', sourceSelection); // re-run  
> selection algorithm when CSS is loaded
>     return null;
>    }
>
> By "CSSisLoaded" I mean state when browser decides the page is ready to  
> be rendered for the first time (all browsers have some form of delaying  
> first page render until external CSS is loaded [or times out]). It's  
> only an optimisation, so details are not important for interoperability.
>
> By mq.cannotFullyEvaluateYet I mean that the media query contains a MQ  
> variable that is not defined, or any other case when browser is not  
> ready to evaluate the media query yet (e.g. MQ refers to viewport size  
> in an iframe that has unknown size).

The iframe problem already exists, so it might be good to delay for that  
case. However, the variable-isn't-declared-yet problem doesn't exist  
today, and we can avoid introducing it.

> If browser decides CSS is not loaded yet and MQ cannot be evaluated yet  
> then it should not load any source yet and re-run source selection  
> algorithm when either CSS is loaded or MQ can be evaluated (e.g.  
> variable becomes defined, viewport size becomes known, etc.).
>
> When CSS is loaded and variable is undefined, then MQ won't match and  
> algorithm proceeds as normal.
> When MQ can be fully evaluated (regardless of whether CSS is loaded),  
> then algorithm proceeds as normal.
>
>
> Also the algorithm is scheduled to run on the next tick whenever child  
> nodes of <picture> are mutated. Asynchronism answers difficult questions  
> like "what happens when <source> node is inserted or removed while  
> source is being selected". Answer: It won't happen. Algorithm is  
> stateless, idempotent, and appears atomic to JavaScript.

This is an attractive property of your proposal, I think. It reduces the  
complexity from what <video> does to be more like what <img srcset> does.  
However, to keep this property, it can't support doing switching based on  
whether the downloaded image is supported (like <video> does).

>>> I don't mind giving authors choice of losing preload scanner if they  
>>> prefer all media queries to be in stylesheets.
>>
>> I do mind giving authors a subtle performance footgun.
>
> That's a footgun under assumption that:
>
> - authors will overwhelmingly choose to use variables in external  
> stylesheets only (and if they do, then that'll prove it's a very  
> desirable feature ;)

That's like saying that layout tables and spacer gifs are a very desirable  
feature because authors overwhelmingly choose to use them.

> - pushing of CSS with HTML via SPDY/HTTP2 or inlining FEO proxies or  
> page preprocessors won't be common enough to mitigate this

So let's say that in the future, the CSS file and HTML file will be  
downloaded in parallel, and will be parsed in parallel as well. The HTML  
parser still would not necessarily be able to resolve the MQ variable when  
it reaches it if it can be declared in CSS.

> - affected pages won't be hiding image markup from preload scanner by  
> using JS-based templating ("MVC" JS apps) or overuse of Web Components  
> (<html><my-cool-app/></html>)  
> <http://tomdale.net/2013/09/progressive-enhancement-is-dead/>

This assumes that such apps will wait with building their DOM until the  
CSS is loaded. Otherwise, the problem is still there.

> - and that 1 RTT delay to start fetching images when CSS is not cached  
> is a big problem

Yeah.

Browsers go to great lengths to have the right set of images as ready as  
possible when it is time for the first paint. That time is usually when  
the external CSS is loaded. You're proposing that the browser can't even  
start downloading the right image until then.

> So my hope is that it won't become a common footgun. Some pages will get  
> increased latency, but that still may be an overall win if easier  
> maintenance convinces authors to use smaller media-specific images  
> instead of taking other easy route and serving largest <img> to all  
> clients.
>
> Since it's a simple declarative markup, servies like YSlow or PageSpeed  
> can advise authors to use faster markup where necessary and tools can  
> fix it automatically.
>
> And browser still have to wait with rendering until external CSS is  
> loaded.

Yes, but when it is loaded, the right images should ideally be loaded as  
well.

> There's strong incentive to fix that, and tools that solve this problem  
> (via inlining or HTTP/2 push) will make external MQ variables zero cost  
> as well.
>
>>> 1. It makes a CSS feature use the HTML syntax.
>>
>> I think this is a weak argument, and it's not obviously a "CSS feature"  
>> any more than e.g. media queries or selectors.
>>
>>> 2. It closes door on using external file for breakpoints ever
>>
>> No, it doesn't. We can still enable the feature in external files in  
>> the future.
>
> I've meant <meta> syntax. Allowing some <meta>-based syntax first and  
> later adding a different, nicer syntax in external stylesheets would be  
> possible, but unnecessarily ugly.

Yeah. (Such a thing has happened: <meta name=viewport> vs @viewport. Maybe  
@viewport was a bad idea...)

>> As far as I can tell the issue can still happen for offline apps if it  
>> uses an external stylesheet.
>
> While technically it could, it would have no impact on user experience,  
> since wasted download simply cannot happen.

It wouldn't waste a network download, right, but you could get an initial  
paint without an image or with the wrong image and then have the right  
image loaded and painted afterwards.

>> 4. It might be desirable to be able to declare scoped media query  
>> variables in <body> for components or something, which could piggyback  
>> on <style scoped>.
>
> AFAIK there have been mixed opinions whether <style scoped> should have  
> scoped @-rules. If scoping of MQ variables isn't difficult to implement,  
> that's great.

OK. Let's ignore this point for now.

-- 
Simon Pieters
Opera Software

Received on Monday, 16 September 2013 08:24:25 UTC