webkit css-on-demand issues

February 11th, 2013. Tagged: CSS, performance

This post brought to you via Facebook engineers Jeff Morrison and Andrey Sukhachev, who discovered and helped isolate the issue.

Use case

Think a "single page app" use case. You click a button. Content comes via XHR. But content is complex (and app is as lazy-loading as possible) and content requires extra CSS. In an external file.

Only when the external CSS arrives should the app show the content. Otherwise content will be weirdly styled.

Execution

Two "modules" (or "widgets") of the app require two different CSS files. Both modules are requested at about the same time. We listen to onload of the CSS files. Expected behavior: whenever a module and its CSS dependency arrive - show that module. Asynchronously. No one cares which module shows first, as long as they show up as soon as possible.

Experimentation

Two modules. Two CSS files. 1st CSS happens to take one second. The second CSS takes 5 seconds.

Test pages: one and two

Here's the end result. The first module is pinky, the second is yellow. All good.

Same with network panel ON:

The question is what does the user see during the ?-mark - between the first CSS is done and the second one is still loading.

Oh, and here's the load() function that runs when the user clicks the button "load" initiating the new modules to appear:

function load() {
  var these = ['class1.css.php', 'class2.css.php'];
  var classes = ['class1', 'class2'];
  var head = document.getElementsByTagName('head')[0];
  
  for (var i = 0; i < these.length; i++) {
    var url = these[i];
    var link = document.createElement('link');
    
    link.type = "text/css";
    link.rel = "stylesheet"
    link.href = url;
    link.onload = (function (i) {
      return function () {
        console.log(these[i]);
        var div = document.createElement('div')
        div.appendChild(document.createTextNode(these[i]));
        result.appendChild(div);
        div.className = classes[i];
        //s = getComputedStyle(result).height;
      }
    }(i));
    
    head.appendChild(link);
  }
 
}

Expected behavior

In FF, whenever the the first CSS is loaded, we see a new module.

Test for yourself (in Firefox)

#1 issue: "efficient" webkit

You know that browsers batch layout and paints tasks because these tend to be expensive. For example they wait for all CSS (even useless print and other @media stylesheets) to arrive and block the rendering of the page. (More on these topics: here, here)

So turns out that here webkit also waits for both CSS files to arrive before rendering anything.

You see in the console we know (in JavaScript) that CSS has arrived. But webkit (chrome, safari, mobile safari) doesn't paint anything, waiting for the second CSS. Bummer!

Issue #2: painting unstyled content

While issue #1 is just a bummer that can be done better for the progressive feedback-y user experience, #2 is a bug. This is the issue that Jeff and Andrey found and were floored.

If there's a paint going on between the two stylesheets, the browser dumps the unstyled content on the page. Ugly stuff.

This was only happening sometimes, but after forming and testing an hypothesis, I was able to distill a reproducible test case. The only change is: after the load of the first CSS, flush the rendering queue by requesting a style information. e.g.

link.onload = function () {
  s = getComputedStyle(dom).height;
}

Repro and file a bug

You can reproduce for yourself. Use chrome and try:

  1. Issue: no paint till all CSS is here
  2. Bug: unstyled paint while waiting for all CSS

I was faced with "registration wall" trying to file a webkit bug, hence this post. Someone please file a bug and feel free to use the provided test cases.

The solution, IMO, is to make webkit behave like FF. No waiting for all CSS. This solves both issues. In the worst case, at least the unstyled bug (issue #2) should be addressed.

Interim solution for web developers: inline CSS required by the module together with the module content.

Thanks for reading!

Tell your friends about this post: Facebook, Twitter, Google+

Sorry, comments disabled and hidden due to excessive spam. Working on restoring the existing comments...

Meanwhile, hit me up on twitter @stoyanstefanov