Browser Performance

In this post we’ll go over the topic of browser performance. We’ll cover tools and techniques to identify, measure and improve performance on the client side of web applications.

Assumptions. Our tests use the Chrome browser, which means the tools likely only make sense for this specific browser (general techniques are agnostic). All the screenshots are taken either on a Linux machine running Gnome or the latest MacOS, though it should be relatively similar to Chrome in other platforms. The Chrome version used is v80.

Setup. I’ll be using simple standalone HTML/JavaScript code. I recommend downloading the folder browser-performance.

Analyzing Performance

FPS Meter

This widget tracks the number of frames per second. To enable it,

In the … icon menu, click on Show console drawer:

Screenshot from 2020-03-16 10-12-37

In the Rendering tab, check the box FPS meter:

Screenshot from 2020-03-16 10-17-55

And it should looks like this:

Screenshot from 2020-03-16 09-46-38

The yellow bar represents 60 FPS, which is the threshold under which the page might look sluggish. The bar chart on the right is a histogram view of the visible line chart. The bars at the bottom represent low FPS and on the top high FPS. The number on the top are the range (max/min FPS).

Initially the numbers will be low because the is not previous data, since it only measure FPS when the page re-renders. To prime the data you need to manually trigger many visual updates, say resizing the window or specific actions on your application:

Screenshot from 2020-03-16 10-26-35

Flame Chart

The flame chart is a very useful visualization to understand performance. It allows identifying expensive functions at a glance (long bars) and dive in into sub-functions quickly.

To use this, download and open (locally) the on_click.html example. Click on the “Performance” tab. Click the record button. Click the “Click Me” button on the page. After some number is displayed, click on Stop on the recording. We should get a chart similar to the one below:

Screenshot from 2020-03-17 10-20-06

Flame chart view

We can see the area chart at the top which is an overview of the processing happening below. The y-axis in that chart is amount of CPU.

The second chart is the flame chart. It has the bars corresponding to function calls (e.g. Event:click) and below that are the functions that were called. We see a generic Function Call because we used a anonymous arrow function like below:

button.onclick = () => {
  expensive();
}

view raw
on_click.js
hosted with ❤ by GitHub

Another thing to notice is that if we zoom enough, we see what looks like multiple calls of the expensive() function but in our code it is a single continuous function. I’m not sure why Chrome does that, but I’m assuming it has to do with the garbage collection kicking in.

Screenshot from 2020-03-17 10-26-02

There are other interesting alternative views at the bottom. The doughnut chart gives a breakdown on types of work being done. In our case it’s just a mix of scripting and idle:

Screenshot from 2020-03-17 10-28-42

The Call Tree view is great for the details. It allows looking at specific call chains and how much each is contributing. In our example, we can see Event: click is an expensive operation but it’s the expensive() function that is the culprit (based on the Self time):

Screenshot from 2020-03-18 09-41-47

To find specific bottlenecks the Bottom-Up view might be more interesting. If the Call Tree shows a nested tree from the root, the Bottom-up view shows the internal nodes but sorted by Self Time:

Screenshot from 2020-03-18 09-44-46

Performance Metrics

When we record a trace that includes a page load, it also includes some standard metrics like First contentful paint (FCP). The definition of some them are here [4].

Screenshot from 2020-03-18 10-00-29

It’s possible to write our own events that show up in Timings. We just need to add markers and  measure duration using the performance API [6]:

performance.mark('expensive-start');
expensive();
performance.mark('expensive-end');
const value = performance.measure(
'expensive-mark',
'expensive-start',
'expensive-end',
);

view raw
measurement.js
hosted with ❤ by GitHub

Then it shows up in the Timeline row:

Screenshot from 2020-03-18 10-21-37

The result of performance.measure() contains useful information:

Screenshot from 2020-03-18 10-21-06

Network

When we first open the Network tab, it immediately starts recording the network activity, but since we want a full-scan of the page requests, let’s stop it, clear the data, start recording again, reload the page and then stop recording to get a report.

The sample below is from imdb.com:

Screen Shot 2020-03-19 at 9.23.48 AM

Network tracing for imdb.com

Similar to the performance tab, this view provides a timeline which we can zoom in. The table below lists all network requests performed by the browser, including static resources like images, JavaScript files and async requests to fetch more data.

We can filter by request type at the top:

Screen Shot 2020-03-19 at 9.29.24 AM

Here we could look for oversized images (though unlikely you’ll need to show a gigantic high-resolution image by default – so look into sending a thumbnail with a link to the larger image) or slow async requests.

Async requests are of type XHR (XmlHttpRequest), so we can filter by that:

Screen Shot 2020-03-19 at 9.33.48 AM

the bars on the last column give an indication of not only the time a request took, but the dependencies. Sometimes an async request doesn’t start until a specific one finishes. If we hover over the bar of the render request, we see:

Screen Shot 2020-03-19 at 9.38.42 AM

It has a nice breakdown on the timings, including DNS lookup and SSL latency (authentication). It also includes queuing time, the amount of time waiting TTFB, and then the content download.

The queuing indicates that the request was ready to go, but needed to wait on some other dependency or due to browser limits. Usually there’s a limit of active XHRs per domain the browser can have (<10) [7].

TTFB means “Time to first byte” – this means that the time it takes to start receiving data. This is indicative of the time the server took processing.

Content download is the time spend getting the resource/data. It’s correlated with the size of the data, network bandwidth/speed.

Measuring Performance

Duration from start

The performance API keeps track of specific events in the page loading lifetime, so we can measure durations compared to a specific point in time.

performance.now() [9] provides the timestamp since the page started to load (in rough terms – see this more precise definition).

Side note: browsers purposefully reduce the precision of these APIs to protect against attacks that rely on high-precision timing information [10].

Custom Durations

As we mentioned in an earlier in measurement.js, we can measure durations. By using performance.mark() and performance.measure() [5].

Benchmarking

We can use the APIs above to create simple benchmarks.

Before we start: always beware of over-optimizing code! You might end up spending a lot of time to improve performance against a specific benchmark, browser or even JavaScript engine version. That’s part of the reason V8 retired the benchmark tool Octane.

That said, if you identify some code in the critical path that executes the same operation on the order of millions of times, it might be worth optimizing it. One example is changing from array methods like forEach() and reduce() to pure for-loops. We can perform several runs of the same function and get the average. I opted for using a random number generator to avoid any optimization related to caching.

The function generated an array with 10 million random floats and calculates the sum. It then runs the code 10 times. The results follow:

  • for loop 12ms
  • reduce 156ms
  • forEach: 208ms

On Firefox it yields

  • for loop 40ms
  • reduce 48ms
  • forEach 64ms

We can see a for loop is ~92% and ~16% faster than reduce on Chrome and Firefox respectively. Some takeaways:

  • It’s highly browser-dependent! Don’t over-optimize code for one browser.
  • Although the relative gains are substantial, the absolute improvements are only 100ms for an array of 10 million items – how practical is this? The creation and allocation of the array probably takes more time than this.
  • This is pure computation, so no DOM access is needed, so consider using Web Workers

Techniques

Here we discuss some techniques to improve performance. The first two are general purpose optimizations but we include here for completeness.

Reduce Algorithmic Complexity

We’ll start with a quote from Donald Knuth [15]:

Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.

Before optimizing code, we should consider optimizing algorithm. This means reducing the big-O complexity. In a recent project, we found several wins by identifying hidden O(n^2) code and converted them to O(n).

Trade Memory for Computation

We often need to make this tradeoff: to improve speed of code (computation) we have to use more memory. This is actually how we improved the O(n^2) to O(n): by keeping a look-up hash table for fast access.

In JavaScript a quick look-up table can be implemented via Object, or ES6 Map and Set. Other techniques involve caching (memoization).

Encode JSON Payloads

In [8] Addy Osmani and Mathias Bynens suggest that if you’re sending large JSON payload via the network, it’s better to send them JSON-encoded instead of JavaScript objects literals:

Because the JSON grammar is much simpler than JavaScript’s grammar, JSON can be parsed more efficiently than JavaScript

Reduce CSS selector complexity

In [17], Lewis suggests reducing the complexity of CSS selectors such as

.box:nth-last-child(-n+1) .title {
/* styles */
}

to

.final-box-title {
/* styles */
}

The reason being:

Roughly 50% of the time used to calculate the computed style for an element is used to match selectors (…)

Use Flexbox

In [18] Lewis suggests that flexbox is more efficient than other layouting alternatives such as float positioning. One caveat made in the article is that is node widely supported. As of 2020 however, that doesn’t seem the case anymore.

Avoid reading DOM properties right after mutating them

In [18] Lewis explains that the Layout phase happens after the JavaScript runs:

Screen Shot 2020-03-28 at 11.51.13 AM

This means that changing a DOM property and reading it afterwards, requires the browser to block and perform the layout before reading the new value. If possible, make the read before the write.

Avoid some advanced CSS effects

This is more of a tradeoff between performance and design. In [19] Lewis mentions that properties such as box-shadow can be expensive.

It’s unclear however which specific CSS properties are expensive.

Use Layers (but sparingly)

In [19] Lewis suggests creating layers for elements that are updated very often, to avoid these updates from causing updates in the rest of the page. One way to force the creation of a layer is:

.moving-element {
will-change: transform;
}

view raw
will_change.css
hosted with ❤ by GitHub

These should only be created if there’re clear improvements in performance, since like indexes in databases, they come with memory and management cost.

Use requestAnimationFrame()

Since JavaScript’s main thread is used for rendering, running expensive function might cause the UI to freeze. In order to prioritize the rendering, we can schedule expensive functions to run we can use requestAnimationFrame(). According to [3]:

When visual changes are happening on screen you want to do your work at the right time for the browser, which is right at the start of the frame. The only way to guarantee that your JavaScript will run at the start of a frame is to use requestAnimationFrame

In [20], Lewis provides a more specific case for the use of requestAnimationFrame(): on scroll input handlers since they block the UI.

There’s one caveat however: Input handlers execute before requestAnimationFrame(), so if it makes style changes and then we read it in requestAnimationFrame, we run into the problem described in Avoid reading DOM properties right after mutating them.

Web Workers

As we discussed previously, web workers run in a separate thread, which prevents your application from freezing because the main thread is blocked. To understand why this happens, it’s important to know the basics of how the JavaScript execution model works.

The major caveat is that web workers don’t have access to the DOM but they do have I/O access (like XHR).

Web Assembly

It’s possible to use lower level languages like C++ and Rust and run them on the browser by compiling them to an intermediate representation called Web Assembly (WASM).

This guide explains how to compile Rust code to Web Assembly, package it as an npm package and execute it on the browser via Webpack. I’ve tried myself and the process is relatively straightforward.

Portability. Web Assembly’s official website [14] claims it is portable across different platforms.

Existing libraries. One of the major selling points of Web Assembly is being able to run existing libraries written in languages like C/C++. In [11] PSPDFKit’s team showcase their port of a PDF renderer that previously had to be done on the server and now can be done by shipping the binary as wasm. They perform some benchmark, getting mixed results in terms of performance but had several follow-up with browser vendors and were able to improve their performance (though no subsequent analysis was available in the post).

Web Assembly’s website [13] lists other use cases.

Performance. The main benefit I can see for starting a project from scratch in say, C++ or Rust, would be performance over JavaScript. JS vs WASM’s site [12] provides a set of computations in JavaScript and wasm compiled from C++. The results are not promising: in most cases Javascript is faster.

GPU via WebGL

WebGL (Web Graphics Library) is a JavaScript library available in most browsers, with the main purpose of rendering 2D and 3D graphics. But because it has access to the GPU (JavaScript doesn’t have direct access), it’s can be used for heavy but highly parallelizable computations such as Machine Learning [16].

If your application requires this sort of processing, WebGL can be an interesting idea.

Conclusion

This was another of the notes on Web Development. The other notes are listed in the Related Posts section below.

In this post we covered a wide variety of tools and techniques regarding performance of client-side applications. We didn’t delve into much detail, but hopefully some of the links will serve as starting pointers for further research.

For me it was particularly interesting to learn more about Web Assembly and I’m interested in studying it further, especially with Rust.

References

[1] Get Started With Analyzing Runtime Performance
[2] Rendering Performance
[3] Optimize JavaScript Execution
[4] User-centric performance metrics
[5] User Timing API
[6] StackOverflow: Using performance.mark() with Chrome dev tools performance tab
[7] StackOverflow: How many concurrent AJAX (XmlHttpRequest) requests are allowed in popular browsers?
[8] The cost of parsing JSON
[9] MDN: performance.now()
[10] Github w3c/hr-time: Reducing the precision of the DOMHighResTimeStamp resolution
[11] A Real-World WebAssembly Benchmark
[12] JS vs WASM
[13] WebAssembly: Use Cases
[14] WebAssembly: Portability
[15] Donald Knuth: Structured Programming with go to Statements
[16] TensorFlow.js: Machine Learning for the Web and Beyond
[17] Reduce the Scope and Complexity of Style Calculations
[18] Avoid Large, Complex Layouts and Layout Thrashing
[19] Simplify Paint Complexity and Reduce Paint Areas
[20] Debounce Your Input Handlers

Related Posts

  • Notes on JavaScript Interpreters – This post focuses on the detail of Chrome’s V8 interpreter. The relationship with performance is that understanding how JavaScript code is executed (e.g. event loop model, JIT) can give us insight on how to optimize it.
  • Web workers – is a browser feature that allows running code outside of the main thread. This can speed up computation as long as no DOM access is needed.
  • Notes on how browsers work – provides a basic overview on the stages of executing a web page, from downloading HTML and Javascript, executing the scripts, constructing the DOM tree and laying out the page. Understanding these steps might give insights on how to better optimize the app.
  • Notes on Javascript Memory Profiling in Google Chrome – memory is related to performance because memory and CPU are the major bounded resources your app has to deal with. Not taking care of memory (e.g. memory leaks) could also exhaust the memory available for the execution engine to make the CPU-Memory tradeoffs we mentioned above.

CSS Layout

Introduction

In a previous post we studied some major components of the browser, including the rendering engine. In this post we’ll dig a bit further on the layout of the render tree and take a look at an important piece of this process: the cascading style sheets or CSS.

To recap, every DOM element is usually represented by one rectangle. The job of the rendering engine is to determine two properties of these rectangles: their size (height, width), position (top, left) and stacking order when they overlap. In this post we’ll see how different CSS properties can affect the resulting rectangle.

Note: we’ll use box and rectangle interchangeably in this post.

CSS

History

hakon

CSS stands for cascading style sheets. It was proposed by Håkon Wium Lie in 1994.

The CSS 1 specification was finished in 1996. CSS 2 was created to address some issues with the previous version in 1997. CSS 3 was started in 1998 but hasn’t been concluded yet! The thing is that CSS 3 is subdivided in modules and each is fairly independent of each other, which resulted in different modules having different phases.

The diagram below provides a good overview on the different modules from CSS and their stage:

css-modules-status

CSS Modules and levels

Wikipedia has an interesting history of the development of CSS, including the initial lack of compliance to the specification which caused a lot of headaches to front-end developers, especially when working with early versions of the Internet Explorer.

It’s interesting to take a look at the early days of CSS because it plays a big role in how it looks today (due to back-compatibility). It’s useful to remember that back in the days when CSS first came around, web pages were generally pure HTML containing mostly text, so a lot of CSS was designed around concepts such as paragraphs and simple images.

In this post we’ll focus on the CSS modules that affect the layout of the DOM elements, in particular the Visual formatting model. According to the spec, these are factors that influence the layout of a box:

* Box dimensions (height and width properties)
* Type of box (display property)
* Positional schemes (position, left and top properties)
* Relationship between elements (hierarchy of DOM tree)
* External information (e.g. window size)

We’ll briefly cover some of these properties and then play with a few examples.

The display property

There are many possible values for the display property. The main ones are none, block, inline and inline-block. I’ve been using flex increasingly but that deserves a post in itself (this article is a great reference).

Display none removes the element from the layout calculation so it’s effectively invisible.

A value of block causes the element to be visually formatted as a block [5] (other values like list-item and table do to). In general a block is a box that starts at a new line and takes the entire width of the parent.

An inline box on the other hand starts from the left of the previous box. It also ignores explicit width and height values and any explicit vertical spacing (i.e.
top/bottom of margin/padding).

The main difference between inline and inline-box is that the latter does account for width and height, and vertical spacing [6].

The position property

There are 5 possible values for the position property: static, relative, absolute, fixed, sticky.

static is the default positioning schema and it follows the normal positioning flow of the page.

A relative positioned element accounts for top and left properties. These are in relation to the parent.

An absolute positioned element is similar to a relative, except that it is removed from the normal layout flow (i.e. other elements ignore its existence when being positioned) and its top and left are in relation to the first positioned ancestor in the DOM tree (or the document if none is). A positioned element is any with position != static.

Here is an example where we only change the inner (red) div to static, relative and absolute, respectively.

Screen Shot 2018-01-13 at 4.25.01 PM

Position: static
<div class="green box200">
<div class="blue box100" style="position: static">A</div>
</div>
Position: relative
<div class="green box200">
<div class="blue box100" style="position: relative">B</div>
</div>
Position: absolute
<div class="green box200" style="position: relative">
<div class="blue box100" style="position: absolute;">
<div class="red box50" style="position: absolute;">C</div>
</div>
</div>

view raw
display.html
hosted with ❤ by GitHub

.box100 {
width: 100px;
height: 100px;
top: 5px;
left: 5px;
}
.box200 {
width: 200px;
height: 200px;
}
.box50 {
width: 50px;
height: 50px;
top: 15px;
left: 15px;
}

view raw
display.css
hosted with ❤ by GitHub

In the first example, A ignores the top/left properties. In the third example is “crosses” the boundary of the middle (blue) box because it’s not positioned.

Note that in the code above we have set overflow to auto. This is a hack to prevent margin-collapsing.

An element with position:fixed is similar to position:absolute, except that instead of having its offset relative to an ancestor with position:relative, usually it’s relative to the viewport. That implies that such element scrolls with the page. The special case happens when one of its ancestors has transform, perspective, or filter set to something other than none, in which case it behaves much like an absolute positioned element.

Finally, as described in [2] an element with position:sticky is treated as relatively positioned until it crosses a specified threshold, at which point it is treated as fixed until it reaches the boundary of its parent.

The float property

When a element is floated, it is taken out of the normal layout flow of the document. It is shifted to the left (assuming a float:left) until it touches the edge of its containing box, or another floated element.

Because float implies the use of the block layout, it modifies the computed value of most of the display values to block.

Floats are better understood in relationship with other types of boxes. Let’s check some examples.

Float + Block

Whether the renderer accounts for a floated element when positioning a new block depends on the block formatting context. For example:

Screen Shot 2018-01-12 at 10.20.07 AM

<div>
<div class="box blue" style="float:left">blue 100×100</div>
<div class="green" style="height: 200px; width:200px">
<span class="yellow">yellow inline</span>
</div>
<div class="box red">red 100×100</div>
</div>

view raw
float_block.html
hosted with ❤ by GitHub

The green block ignores the presence of the blue block but its child (yellow) does not. That’s because blue and yellow are in different block formatting contexts.

Float + Inline

Analogous to a block context, an inline formatting context is a set of inline elements. In such context, the boxes are laid out horizontally. Each “row” of inline elements is called a line box. In the presence of floats, the spec states the following:

In general, the left edge of a line box touches the left edge of its containing block and the right edge touches the right edge of its containing block. However, floating boxes may come between the containing block edge and the line box edge. Thus, although line boxes in the same inline formatting context generally have the same width (that of the containing block), they may vary in width if available horizontal space is reduced due to floats.

We can see an example of that in here. In this example the first three line boxes have a shorter width than the fourth one due to the presence of the float element.

Screen Shot 2018-01-19 at 11.20.05 PM

<div class="box200 green">
<span>Lorem ipsum dolor</span>
<span class="yellow box50" style="float:left">float</span>
<span>sit amet</span>
<span>consectetur adipiscing elit</span>
<span>sed do eiusmod tempor</span>
<span>incididunt ut labore et</span>
</div>

view raw
float_inline.html
hosted with ❤ by GitHub

https://gist.github.com/kunigami/2f5dd30b23c6f8d6184abec5d9f0b772&nbsp;

Clear

When applied to a floating element, clear moves the margin edge of the element below the margin edge of all relevant floats (depending on whether it’s clear left, right or both). Here’s an example:

Screen Shot 2018-01-15 at 7.49.52 PM

<div style="width: 200px; background-color: green">
<div class="red" style="clear:left">Clear left</div>
<div class="blue" style="float: left">Left</div>
<div class="yellow" style="float: right">Right</div>
<div class="blue" style="float: left">Left</div>
<div class="red" style="clear:right">Clear right</div>
<div class="yellow" style="float: right">Right</div>
</div>

view raw
clear.html
hosted with ❤ by GitHub

The z-index property

Besides determining the size and position of boxes, the layout engine needs to determine how to handle overlapping. The boxes ordering is transitive, meaning that if a box A is under B, and B is under C, A has to be under C.

The main attribute to control the stack order of elements is the z-index property (in reference of the z-axis, commonly used as the “depth” dimension in 3D). But this number only applies for boxes under the same class. As we’ll see now, it’s more complicated than it seems.

First, we need to define the concept of stacking context. A stacking context encompasses a set of DOM elements which can be compared to each other. The order of the stack context always take precedence over individual orders of elements within a stack context. For example, imagine that we have 2 stacking contexts:

Stack context 1: [A -> B -> C -> D]
Stack context 2: [E -> F -> G]
Stack context 1 -> Stack context 2

The arrow (->) represents that the element on the left is on top of the element on the right. Because A, B, C and D belong to context 1, all of them are placed over E, F and G, no matter how big the z-index of elements in the second group are.

Another way to see it is that a stacking context defines an “atomic position”, meaning that elements from outside it cannot be placed in between its elements. It has to be either above or below.

In the DOM tree, if a node E satisfies some conditions, it starts a new stack context, which means that all elements in the DOM subtree of that element will be under that stack context (we say E is the root of such stacking context). Note that a stack context can contain other stack contexts. A few properties that cause a stacking context to be formed are:

* position: absolute or relative and z-index other than auto
* position: fixed or sticky
* opacity less than 1.
* others

Note that position: static ignores z-indexes, so a corollary is that the use of z-index effectively creates a new stacking context.

Within a stacking context, the order of elements is defined by the spec. Here’s a simplified version, from bottom to top:

* The background and borders of the element forming the stacking context;
* Stacking contexts with negative z-indexes;
* Non-positioned block boxes;
* Non-positioned floats;
* Non-positioned inlined boxes;
* Stacking contexts with positive z-indexes;

Another corollary is that a parent is always rendered below its children due to the first rule above.

Here is an example with elements of each of these categories.

Screen Shot 2018-01-18 at 8.51.44 PM

<div class="root_stacking_context">
<div class="box stack_context_negative_index">#1</div>
<div class="box non_positioned non_inline">#2</div>
<div class="box non_positioned_float">#3</div>
<div class="box non_positioned inline">#4</div>
<div class="box stack_context_positive_index">#5</div>
</div>

view raw
positions.html
hosted with ❤ by GitHub

.box {
height: 100px;
width: 100px;
}
.root_stacking_context {
border: 1px solid;
height: 300px;
width: 300px;
}
.stack_context_negative_index {
z-index: -1;
background-color: red;
height: 200px;
position: absolute;
text-align: right;
top: 15px;
left: 45px;
}
.non_positioned.non_inline {
background-color: green;
top: 30px;
left: 30px;
margin-bottom: -50px;
}
.non_positioned_float {
float: left;
background-color: cyan;
margin-left: 15px;
}
.non_positioned.inline {
background-color: orange;
display: inline-block;
margin-left: -35px;
margin-top: 15px;
}
.stack_context_positive_index {
z-index: 1;
background-color: yellow;
position: absolute;
top: 100px;
left: 60px;
}

view raw
positions.css
hosted with ❤ by GitHub

Here is an interesting example with stacking contexts.

Screen Shot 2018-01-19 at 8.47.39 PM

<div class="box red">
<div class="box green" style="margin: 25px; z-index: 11" />
</div>
<div
class="box purple"
style="margin-top: -100px; margin-left: 50px; z-index: 10">
<div class="box pink" style="margin: 25px; z-index: 9999" />
</div>
<div
class="box black"
style="margin-top: -100px; margin-left: 50px; z-index: 9"
/>

The red box is the parent, so it has to be under the green box. The green, purple and black boxes are in the same stacking-context so they are ordered based on the z-indexes (since red doesn’t start a stack context, green belongs to the top-level stack context). Finally, note how pink has the highest z-index, but is still placed under green because it belongs to the stack context of purple, which is placed under green.

References

[1] MDN Web Docs: CSS display
[2] MDN Web Docs: CSS Position
[3] MDN Web Docs: CSS Float
[4] A Complete Guide to Flexbox
[5] MDN Web Docs: Block formatting context
[6] StackOverflow: display: inline vs inline-block
[7] All about floats
[8] CSS 2.1 Specification: Visual formatting model

Conclusion

My main goal with this study was to learn more about the layout algorithm used by render engines but this turned out to be an analysis of some CSS properties. It was an interesting exercise anyway. I work with CSS on a daily basis but often times I don’t take the time to read the spec or understanding why a given set of properties behave in a certain way.