Re: Media Query Variables

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.

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

>> If this rule triggers syntax error in external stylesheets I think it  
>> would be acceptable compromise, but it's not pretty :/
>
> 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, and it may be impossible to add support for  
variables in external stylesheets later.

>>> Making the image load block on external stylesheets is not acceptable.  
>>> I think we should design a solution that enables the browser to  
>>> download the correct resource as soon as it sees it without blocking  
>>> on another resource.
>>
>> 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:

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.

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.

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.


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;
  }
  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).

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.

>> 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 ;)
- pushing of CSS with HTML via SPDY/HTTP2 or inlining FEO proxies or page  
preprocessors won't be common enough to mitigate this
- 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/>
- and that 1 RTT delay to start fetching images when CSS is not cached is  
a big problem

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. 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.

>> 3. Not all applications need preload scanner. For example app.ft.com is  
>> a JS-based offline app. It needs responsive images, but not preload  
>> scanner. It would be shame if it had to use worse syntax and less  
>> maintainable solution to enable optimization it does not need.
>
> 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.

> 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.

-- 
regards, Kornel

Received on Saturday, 14 September 2013 21:22:55 UTC