How Is CSS Applied by The Browser, and Are Repaints Affected by It

How is CSS applied by the browser, and are repaints affected by it?

How does the browser take the rules in this stylesheet and apply it to the HTML?

Typically this is done in a streaming fashion. The browser reads the HTML tags as a stream, and applies what rules it can to the elements it has seen so far. (Obviously this is a simplification.)

An interesting related Q&A: Use CSS selectors to collect HTML elements from a streaming parser (e.g. SAX stream) (a diversion while I search for the article I have in mind).


Ah, here it is: Why we don't have a parent selector.

We often think of our pages as these full and complete documents full of elements and content. However, browsers are designed to handle documents like a stream. They begin to receive the document from the server and can render the document before it has completely downloaded. Each node is evaluated and rendered to the viewport as it is received.

Take a look at the body of an example document:

<body>
<div id="content">
<div class="module intro">
<p>Lorem Ipsum</p>
</div>
<div class="module">
<p>Lorem Ipsum</p>
<p>Lorem Ipsum</p>
<p>Lorem Ipsum <span>Test</span></p>
</div>
</div>
</body>

The browser starts at the top and sees a body element. At this point,
it thinks it's empty. It hasn't evaluated anything else. The browser
will determine what the computed styles are and apply them to the
element. What is the font, the color, the line height? After it
figures this out, it paints it to the screen.

Next, it sees a div element with an ID of content. Again, at this
point, it thinks it's empty. It hasn't evaluated anything else. The
browser figures out the styles and then the div gets painted. The
browser will determine if it needs to repaint the body—did the element
get wider or taller? (I suspect there are other considerations but
width and height changes are the most common effects child elements
have on their parents.)

This process continues on until it reaches the end of the document.

CSS gets evaluated from right to left.


To determine whether a CSS rule applies to a particular element, it
starts from the right of the rule and works it's way left.

If you have a rule like body div#content p { color: #003366; } then
for every element—as it gets rendered to the page—it'll first ask if
it's a paragraph element. If it is, it'll work its way up the DOM and
ask if it's a div with an ID of content. If it finds what it's looking
for, it'll continue its way up the DOM until it reaches the body.

By working right to left, the browser can determine whether a rule
applies to this particular element that it is trying to paint to the
viewport much faster. To determine which rule is more or less
performant, you need to figure out how many nodes need to be evaluated
to determine whether a style can be applied to an element.


So why was the stylesheet content not applied progressively (green first, then red)?

I think the answer is that external stylesheets are parsed as they are downloaded, but not applied until the entire stylesheet has been parsed. Surely, in parsing a stylesheet, the browser optimizes away unnecessary and redundant CSS rules.

I don't have any proof to back that up right now, but that explanation sounds reasonable to me and agrees with what you're seeing, both with external and inline styles.

Detect when all CSS rules have been applied and painted

I'd say the most reliable solution would be to add a small delay after the css loaded event.

Something like this pseudo-code:

myCssTag.onload(function(){
setTimeout(doStuffAfterRender, 100);
});

browser repaints and performance

The way to answer performance questions is to perform benchmarks. If you want to know how much performance overhead there is to display:none elements, do the following:

Test 1: Load the page with all the display:none elements, measure the repaint speed.

Test 2: Modify the page so that the display:none elements are removed completely, load this page, and measure the repaint speed.

The difference between the two tests is the repaint overhead of display:none elements. Hopefully it will be minimal.

What actions and events cause a browser to repaint its entire viewport?

AHA! Ah-effing-ha! (forgive the enthusiasm)

The issue is that I was using the box-shadow css property to frame my page. It takes longer to reflow/repaint content when the browser needs to calculate that shadow on each change (~100ms vs ~1ms). And when using wmd-editor, you're updating the dom on each keypress, so that difference adds up. And the effect is most exaggerated when the browser is maximized, as it recalculates the entire viewport.

So maybe that's one of the reasons stackoverflow doesn't have any frames or shadows on the page... just clean edges.

You can see what I mean at this example page. Open it up in firefox, maximized the page, and start typing away. Now use firebug to remove the box-shadow property on the body element, close firebug back up and try again. Big difference.

Thanks to Balpha for his comment, which was spot on.

Does the order of rules in a CSS stylesheet affect rendering speed?

After some more testing and reading I came to the following conclusion, no, it does not matter. Even after some ‘extreme’ testing, I could not find anything that supports the idea that the order matters.

There were no 'flashed of unstyled content' or the likes, it just took way longer to load the page ( way way longer :D )

Tests I ran
I created a test page with 60.000 div elements, each having a unique ID attribute. Each of these ID’s had their own css rule applied to it. Below that I had a single span element with a CLASS attribute, which was also had a css rule linked to it.

These tests created a html file of 2MB with a corresponding css file of 6MB.

At first I attempted these tests with 1.000.000 divs and css rules, but Firefox did not approve and started crying, begging me to stop.

I generated these elements and their css with the following simple php snippets.

<?PHP

for ($i = 0; $i < 60000; $i++) {
echo "
#test$i {
position: absolute;
width: 1px;
height: 1px;
top: " . $i . "px;
left: 0;
background: #000;
} <br />
";
}

?>

And

<?PHP

for ($i = 0; $i < 60000; $i++) {
echo "
<div id=\"test$i\"></div>
";
}

?>

The result was put in a html and css file afterwards to check the results.

Mind you, my browser ( Firefox 5 ) really did not appreciate me playing around with this, it really had some issues generating the output, the occasional this program is not responding message was not afraid to show it's face.

These tests were ran on a localhost, ran by a simple XAMPP installation, it might be possible that external servers result in a different resultset, but I am currently unable to test that.

I tested a few variations on the above:

  • Placing the element before all the generated divs, in the
    middle and at the end
  • Placing the span’s css definition before, in the middle or at the end
    of the css file.

Oh and may I suggest: http://www.youtube.com/watch?v=a2_6bGNZ7bA while it doesn't exactly cover this question, it does provide some interesting details about how Firefox ( and possibly other browsers )work with the stuff we throw at it.

Repaint slowdown with CSS via Javascript in webkit browsers

Well, think I've managed to figure it out! Just so you know, original post links don't reflect the changes as I've done them on my localhost environment.

Absolutely positioning the slides container has fixed the problem that was occurring with repaint speeds after the transition had taken place (whilst applying CSS properties). Obviously taking them out of the DOM has done the trick, allowing painting to take place much more efficiently.

I originally didn't try this too much because I knew this would add a lot of work to the resizing functionality. I had originally intended to not resize at all in JS, and rely on percentages to do the dirty work. Absolutely positioning the container would cause the slideshow viewport to collapse, rendering the native resizing useless.

However, I was already having problems with sub-pixel rendering in other browsers anyway, so I guess it was time to bite the bullet and rely on fixed pixel values. I then used JS to handle the resizing, using the window resize event. All seems good, however the slideshow was still collapsed due to the positioning. Assigning height values wasn't working correctly, so was at a bit of a loss.

Thankfully, I came across a neat little trick of setting the 'padding-top' of the slideshow viewport to a percentage value, dynamically calculated (desired slideshow height, set in the settings panel for this script, divided by desired width). As padding-top percentages are relative to the width of the element, this did a great job of providing responsive height and correcting the viewport again (no longer looking collapsed).

Here is some info on using padding-top for responsive elements that maintain aspect ratio. Great little trick: http://f6design.com/projects/responsive-aspect-ratio/

All is good now, and things are working well in iOS and webkit browsers. Everything is extremely quick and working as it should. Four days later, and it is finally figured out. Not happy about having to resort to JS for resizing, but I guess it was always going to happen due to percentage inconsistencies between browsers. Lots of decimals = no good!

Thanks to all who tried to point me in the right direction. Definitely got me thinking, and learned a lot of debugging skills that I can use again to make sure transitions are performing well. Thanks again!

Why does Chrome not need to repaint the entire layer on a change?

So, the confusing bit here was the fact that the Paint flashing of the dev tools, only flashes the part of the layer that gets invalidated from the previous frame (so if one absolute positioned square starts getting smaller, the invalidated area between frames is an area with the dimensions and coordinates that the square had in the previous frame)

However, internally the whole layer gets repainted, no matter how big or small the invalidated parts are between frames.

So, for example, a blinking cursor will look small on Paint Flashing, but in reality the entire layer needs to be repainted.

Indeed, if we open up the Performance panel and enable the Advanced Painting Instrumentation option, we can see that between the square transitions the whole layer gets painted in both scenarios described in the question.

Chrome Paint analysis 1

Chrome Paint analysis 2

Sources

https://twitter.com/paul_irish/status/971196975013027840

https://twitter.com/paul_irish/status/971196996924030977

https://twitter.com/paul_irish/status/971197842713800704

Some observations

If we were to minimise the Layout and Painting steps to make as few operations as possible we should separate the red square and the yellow button to their own render layers.

This way interacting with the button and resizing the square will only affect their respective layers and will not cause a repaint to the background layer (which includes the background and the blue square).

Will adding a class without style information cause reflow?

No.

It will cause a style application invalidation, but when the browser performs a re-evaluation it will see that the element's computed properties that affect layout have not been altered, so it won't trigger a layout - or even a repaint.

Remember that different CSS properties may trigger a relayout when changed (such as width), others only trigger a repaint (such as background-color), and others do nothing at all (such as voice-family, which is in the (now obsolete) Aural module which is independent of the visual-formatting model).

How can I force WebKit to redraw/repaint to propagate style changes?

I found some complicated suggestions and many simple ones that didn’t work, but a comment to one of them by Vasil Dinkov provided a simple solution to force a redraw/repaint that works just fine:

sel.style.display='none';
sel.offsetHeight; // no need to store this anywhere, the reference is enough
sel.style.display='';

I’ll let someone else comment if it works for styles other than “block”.

Thanks, Vasil!



Related Topics



Leave a reply



Submit