As you can start to see, there are many opportunities to share style contexts and reap massive memory optimizations.

This is especially true with sibling elements. Sibling elements can often share ENTIRE STYLE CONTEXTS. E.g. <li> elements. They are often identically styled, so they typically share the same style context.

Difficulty #2: Matching Elements to Selectors

There are 3 sources for CSS styles:

CSS Rules (external via <link> or internal via <style> ) Inline Style Attributes ( <div style="background-color: goldenrod;"> ) Specific Style Attributes ( bgcolor="red" , valign="baseline" , etc.)

These are deprecated. Don’t use them.

When it comes to matching elements with styles, #2 and #3 are trivial. The styles apply to the element they’re attached to.

#1 is worthy of discussion — from a performance perspective.

The styles in CSS rulesets use selectors to match elements. Determining which elements to match is the easy part. id selectors match elements with the corresponding id . class selectors match elements with the corresponding class . Type selectors ( p , div , dl , progress ) match elements of the corresponding type. So on and so forth.

But how to match elements is the hard part. If you match elements naïvely, you would have some major performance problems.

Example of Naïve Solution: For every CSS ruleset, loop through every element to determine which ones it should apply to.

This would yield awful performance.

So how do browsers match elements to selectors?

Hashmaps are perf(ect) for perf(ormance)!

Before the browser begins parsing CSS rules, 4 hashmaps are created. These hashmaps will store the CSS rules. Each hashmap stores a different category of CSS rules, based on their selectors.

ID-selector CSS Rules

Class-selector CSS Rules

Type-selector CSS Rules

Miscellaneous-selector CSS Rules

This drastically improves the performance of element-selector matching.

You simply lookup an element’s corresponding CSS rulesets by using its ID/Class/Type/etc. as keys.

Webkit and Gecko both implement this optimization.

Difficulty #3: Applying Styles in the Correct Order

Very frequently, an element’s style property is declared more than once.

Example:

div p {

color: goldenrod;

} p {

color: red;

}

In what order do you apply these?

If you apply rule 1 first, then rule 2 second, you’ll have color: red .

color: goldenrod would be overwritten.

If you apply rule 2 first, then rule 1 second, you’ll have color: goldenrod . color: red would be overwritten.

The order of style-application is known as the cascading order. It is fundamental to CSS’s nature. It’s even in the name!

According to the CSS2 Specification, § 6.4.1 Cascading Order, the cascading order is (in ascending priority):

User Agent Declarations (Browser Defaults) User Normal Declarations

(Browser users can actually set custom styles for themselves. For example, a vision-impaired user might set a default of font-size: 30px for all websites they visit. This is what a “User” declaration is. Don’t confuse this with “User Agent” declarations. “User Agent” means browser.) Author Normal Declarations

(The styles that you, a page author/web developer, set.) Author !important Declarations User !important Declarations

(This is to provide the user with ultimate control over presentation. This is especially useful for people with accessibility demands.)

If two declarations fit the same order, (E.g. they are both User Normal Declarations), then sort by SPECIFICITY.

If two declarations fit the same order AND specificity, then sort by position in the stylesheet(s). The declaration that occurs later in the stylesheet(s) shall apply.

Specificity

The specificity of a declaration is a 4 digit number, a b c d .

Example:

#foo .bar > [name="baz"]::first-line {} /* Specificity: 0 1 2 1 */

You concatenate these numbers ( 0 1 2 1 => 121 ). Then you sort these numbers in ascending order. The higher number wins.

How do we calculate these numbers?

a :

Set to 1 if the declaration originates from a style="" attribute.

Set to 0 otherwise.

: Set to if the declaration originates from a attribute. Set to otherwise. b :

Count the # of ID selectors in the overall selector. That number is the value for b .

: Count the # of ID selectors in the overall selector. That number is the value for . c :

Count the # of class selectors, attribute selectors, and pseudo-classes in the overall selector. That number is the value for c .

: Count the # of class selectors, attribute selectors, and pseudo-classes in the overall selector. That number is the value for . d :

Count the # of type selectors and pseudo-elements in the overall selector. That number is the value for d .

With this knowledge, here are some examples (taken from the CSS2 spec).

* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */

li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */

li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */

ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */

ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */

h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */

ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */

li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */

#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */

style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

VERY IMPORTANT POINT: The base/radix of the specificity is NOT NECESSARILY 10 . You could have a specificity of 0EF9 , which would hypothetically have a radix of 16 (Hexadecimal).

The specificity radix grows as needed. If you wrote a selector 297 type selectors within it, then your radix would be 297 (assuming no other count surpasses 297). If your largest count in a given digit were only 3, then your radix would be 3.

If the radix were locked at 10, you would have carry-over to the left digit. That would create confusing situations.

Gradual Process

Recall that rendering+painting is a gradual process. The browser won’t wait to parse every HTML document and stylesheet before rendering+painting.

If a given DOM node’s styles haven’t loaded (due to network latency) by the time <DOM node>.attach() is called, default placeholder-styles are used. The DOM node is flagged and its styles will update when they load.

5. Layout

Dirty Bit System

Global & Local Layout

Asynchronous & Synchronous Layout

The Layout Process

Upon creating a render object, it does not have a position or dimensions.

Layout is the calculation of these values.

HTML uses an intuitive flow-based layout model by default. This means that elements are laid-out in the order that they appear: left-to-right, top-to-bottom.

Typically, elements “later” in the flow don’t affect elements “earlier” in the flow. So, for simple situations, only a single pass is necessary to lay out all elements. Some layout models such as display: table (and I think display: flex and display: grid ) may require multiple passes.

For positioning, the coordinate system is relative to the root render object. Typically, this is the HTMLDocument 's render object. ( HTMLDocument is the parent node of HTMLHtmlElement , the DOM node for <html> . It always wraps the HTMLHtmlElement .)

A render object’s position is the position of its top-left corner.

The root render object’s position is (0,0) , and its dimensions are the viewport’s dimensions. (The viewport is the portion of the browser window that displays webpages.)

Layout is a recursive process.

Each render object has a layout() method. So first, the root render object is laid-out. Then, it lays out its children by calling their layout() methods. Its children layout their children (RRO’s grandchildren). So on and so forth.

DIRTY BIT SYSTEM

Consider a webpage in the middle of its lifecycle — after its initial layout.

Suppose a DOM node’s style is changed. This change will affect layout. How do you handle this? Do you perform a Global relayout? Or do you perform a local relayout on the relevant render objects?

Ideally, you’d just perform a local layout. (But sometimes, you need to perform a global layout, because the change affects neighbor render objects).

To perform a local layout, you need the ability to distinguish dirty render objects from clean ones.

To attain this ability, browsers flag specific render objects as dirty if they require layout due to some change.

Actually, there are 2 types of dirty flags: dirty self & dirty children.

A dirty-self flag indicates that the render object itself requires layout.

A dirty-children flag indicates that 1+ of the render object’s children require layout. The render object itself may be clean.

GLOBAL & LOCAL LAYOUT

Global layout is required when changes affect all render objects.

Examples:

A global style change, like changing all font-family or font-size values.

or values. Resizing the screen.

(Bolded b/c it’s not obvious that a non-stylistic change could force global layout.)

Local layout only targets dirty render objects. It’s performed asynchronously by the rendering engine, which is single-threaded.

ASYNCHRONOUS & SYNCHRONOUS LAYOUT

Global layouts are almost always synchronous.

Local layouts are performed asynchronously and in batches by default. The rendering engine will attempt to group several pending layouts, then apply them asynchronously in 1 sweep. This is to improve performance. Applying many individual layout + paint procedures would yield poor performance. Whereas applying all the layouts, then painting once is better.

However, there is a situation that forces local layouts to perform synchronously:

scripts demanding style information, like clientWidth .

In such situations, the script must immediately know style information that is up-to-date. The pending layout cannot be performed asynchronously at a later time. It must be performed now because a <script> is waiting for it, in order to obtain up-to-date style information.

Therefore, because the rendering engine and the JS engine share the same thread, the demanding script will immediately halt, wait for the layout to apply, then resume.

Poorly written scripts can cause many synchronous, individual local layouts. This is known as layout thrashing. Avoid this when possible.

THE LAYOUT PROCESS

Layout is typically performed as such. For each render object:

The given render object determines its width.

(This derives from the containing block’s width, the render object’s width style-property, and the render object’s margin and borders.) For each of its child render objects,

1. Position the child render object.

2. Layout the child render object if needed.

Often, when the webpage is en media res, child render objects are clean; their dimensions don’t need to be recalculated. But if they are dirty, or if a global layout is occurring, they will need relayout. Sum the child render objects’ heights. Set the parent render object’s height to this cumulative height. Flag the given render object as clean.

6. Painting

Global & Local Painting

The Painting Order

Webkit’s Bitmap Deltas

The Rendering Engine’s Threads

Event Loop

GLOBAL & LOCAL PAINTING

Similar to the rendering process, painting can either be global or local. Regions of the content are flagged as dirty to enable local paints. Then, the rendering engine invokes the UI Backend component of the browser to actually repaint the dirty regions. Recall that the UI Backend relies on the host OS’s API to paint content onto the screen.

THE PAINTING ORDER

Render objects have many layers on the z-axis. Their painting order (from back to front) is:

Background Color Background Image Border Children Render Objects Outline

WEBKIT’S BITMAP DELTAS

For a given painting-region, WebKit will record it as a bitmap. When the painting-region becomes dirty, WebKit won’t repaint the entire region. It will only paint the deltas from the old bitmap to the new bitmap. This provides some optimization.

THE RENDERING ENGINE’S THREADS

As I mentioned earlier, the rendering engine is single-threaded. Also, it shared the same thread as the JS Engine of a browser. This godlike thread is known as the browser’s main thread. Networking occurs in a separate thread.

Relevant section from https://www.chromium.org/developers/the-rendering-critical-path#TOC-Browser-Thread-Architecture

EVENT LOOP

The browser’s main thread uses an event loop to handle asynchrony. It’s an infinite loop that pulls tasks such as layout, painting, and JS execution from a message queue and processes them.

7. CSS Visual Model

The Canvas

CSS Box Model

Positioning Scheme

Box Types

Positioning

THE CANVAS

The Canvas is a term used in the CSS specification. It describes the “theoretically infinite space” where content is painted to. You view the canvas via your viewport.

CSS BOX MODEL

The Box Model specifies the structure for CSS Boxes created for each rendered (e.g. not display: none ) HTML element in a document.

There are 4 components:

Content

Typically text. Can be images/video/etc. for replaced elements. Padding

Whitespace between the content and the border. Border Margin

Whitespace outside the border. Mostly used to distance a CSS box from other boxes.

The following image depicts the fundamental CSS box. There are other derivatives of this box, but this is the basic one.

POSITIONING SCHEME

The Positioning Scheme is one of the most important factors in CSS layout. There are 3 positioning schemes:

Normal Flow

This is the “intuitive” flow-based layout I discussed earlier. Each CSS box’s position corresponds to its HTML element’s position in the DOM. Floats

The box is removed from normal flow, then moved as far right/left as possible. Adjacent content may flow around the box. Absolute Positioning

The box is removed from normal flow entirely (it does not affect adjacent content at all). It is assigned a position relative to the containing block.

The choice of positioning scheme is influenced by the position and float properties.