Computed Style Calculation To Free JavaScript Execution Time

As per webdev, Computed Style Calculation is the process of changing the DOM, by adding and removing elements, changing attributes, and classes, or through animation. Eventually, which will all cause the browser to recalculate element styles and, in many cases, layout (or reflow) the page, or parts of it. Perse, JavaScript is often the trigger for visual changes.

Sometimes that’s directly through style manipulations, and sometimes it’s calculations that will result in visual changes, like searching or sorting some data. Badly-timed or long-running JavaScript can be a common cause of performance issues, and you should look to minimize its impact where you can. The first part is to create a set of matching selectors.

Essentially, the browser figures out which classes, pseudo-selectors, and IDs apply to any given element. The second part of the process involves taking all the style rules from the matching selectors and figuring out what final styles the element has. In Blink (Chrome and Opera’s rendering engine) these processes are today at least, roughly equivalent in cost.

Resource Reference: 13 Simple Steps To Improve Your WordPress Website Performance

Roughly 50% of the time used to calculate the computed style for an element is used to match selectors, and the other half of the time is used for constructing the RenderStyle (computed style representation) from the matched rules. May it be Rune Lillesveen, Opera/Style Invalidation in Blink, and so on… To be on the safe side, it’s good to apply the TL;DR rules.

On one hand, it will help reduce the complexity of your selectors;— use a class-centric methodology like BEM in this case. While on the other hand, it will reduce the number of elements on which style calculation must be calculated. All this is in regard to the new Core Web Vitals guideline by Google that is found through its PageSpeed Insights analytics access tool.

DOM Mutations And Computed Style Calculation

Mutating the DOM, either by adding or removing nodes, or modifying attributes and input states, will change the computed style for a set of elements in the DOM. The reason is that the evaluation of selectors will change due to these mutations. Modifying the tree structure will make selectors change evaluation because of combinators and structural pseudo-classes.

As such, modifying attributes and input state will change evaluation because of pseudo-classes, id selectors, class selectors, and attribute selectors. The tree structure changes will cause render tree reattachment, hence full style recalc, for the removed/inserted node and its whole subtree. That type of change will also cause recalculation of computed style calculation.

Resource Reference: BEM (Block, Element, Modifier)

Especially, the calculation for siblings of the inserted/removed node because of adjacent combinators and structural pseudo-classes affecting those siblings. The number of nodes that are affected by a DOM mutation depends on the selectors present in the author and UA stylesheets for the document — with the currently implemented CSS selectors in Blink.

For such reasons, there are a few elements that can possibly have their computed style affected by a change in a given feature.

They include:
  • The element itself
  • Its descendants
  • All succeeding siblings and their descendants
The worst case can be expressed with this rule:

.a, .a *, .a ~ *, .a ~ * * { }

When setting the class attribute to “a” on an element, the first selector will select the element itself, the second all its descendants, the third all its siblings, and the fourth all sibling descendants.

Consequently, you have to recalculate the computed style for all those descendants and siblings. In the common case, however, when changing a class, id, or other attributes on an element, a computed style recalc is necessary for far fewer elements.

Take this example:

.a .b {}

Only descendants of the element are affected when setting class=”a”, and only elements which have a class attribute that contains the class “b”.

The Main Computed Style Calculation Performance Aspects

The brute force approach to computed style recalculation, given the possible set of affected siblings and descendants in the previous section, is to recalculate all elements in the worst-case scenario. That requires no knowledge about the CSS selectors that apply in a given document. At the beginning of 2014 that was pretty close to what the Blink implementation did.

The meta-data stored were hash sets where an id, class, or attribute was in a hash set if it occurred in a selector if an element could possibly match a selector containing an adjacent combinator, and a document-wide maximum number of consecutive adjacent combinators. In practice, that meant we could skip sibling subtree recalculation in most cases.

But, all descendants of the modified element always got their computed styles recalculated. For instance, that meant changing anything on the BODY element caused a full document to recalculate. The performance goal of the Blink project is to be able to run web content at 60fps on a mobile phone. Eventually, this means we have 16ms per frame.

Particularly, to handle input, execute scripts and execute the rendering pipeline for changes done by scripts through style recalculation, render tree building, layout, compositing, painting, and pushing changes to the graphics hardware. So style recalculation can only use a fraction of those 16ms. In order to reach that goal, the brute force solution is not good enough.

Learn More: CSS Style Calculation In Blink

At the other end of the scale, you can minimize the number of elements having their style recalculated by storing the set of selectors that will change evaluation for each possible attribute and state change and recalculate the computed style for each element that matches at least one of those selectors against the set of the descendants and the sibling forest.

At the time of writing, roughly 50% of the time used to calculate the computed style for an element is used to match selectors, and the other half of the time is used for constructing the RenderStyle (computed style representation) from the matched rules. Of course, we can match selectors to figure out exactly which elements need to have the style recalculated.

And then, thereafter, consider doing a full match. Unfortunately, all this is probably too expensive too. We landed on using what we call descendant invalidation sets to store meta-data about selectors and use those sets in a process called style invalidation to decide which elements need to have their computed styles recalculated.

Descendant Invalidation Sets:
  • Constructing Invalidation Sets
  • Scheduling Invalidations
  • The Style Invalidation and Style Recalculation

One way is to do it indirectly through Descendant Invalidation Sets. An Adjacent Invalidation Set could contain the set of properties P found in the rightmost compound selector of an adjacent combinator chain.

When such an invalidation set exists for a given change, schedule Descendant Invalidation Sets for the members of the Adjacent Invalidation Set on siblings of the modified element.

The Computed Style Calculation Process

In modern browsers, this tends to be much less of an issue, because the browser doesn’t necessarily need to check all the elements potentially affected by a change. Older browsers, on the other hand, aren’t necessarily as optimized for such tasks. Where you can you should reduce the number of invalidated elements.

If you’re into Web Components it’s worth noting that style calculations here are a little different, since by default styles do not cross the Shadow DOM boundary, and are scoped to individual components rather than the tree as a whole. Overall, however, the same concept still applies: smaller trees with simpler rules are more efficiently processed than large trees or complex rules.

JavaScript | Window getComputedStyle() Method:

  • First, the getComputedStyle() method is used to get all the computed CSS properties and values of the specified element.
  • Secondly, the use of computed style is displaying the element after stylings from multiple sources have been applied.
  • Lastly, the getComputedStyle() method returns a CSSStyleDeclaration object.
The general syntax is as follows:
window.getComputedStyle(element, pseudoElement)
Below are the key parameters:
  • element: The element to get the computed style for
  • pseudoElement: A pseudo-element to get. this is an optional parameter.

That said, it’s important to realize, that when your JavaScript takes a long time to execute, it slows down your overall website pages/blog posts performance in several ways. Some of which might affect your website performance in a great way.

Consider some of the following aspects:
  • Network cost: More bytes equals longer download times.
  • The parse and the compile cost: JavaScript gets parsed and compiled on the main thread. When the main thread is busy, the page can’t respond to user input.
  • Execution cost: JavaScript is also executed on the main thread. If your page runs a lot of code before it’s really needed, that also delays your Time To Interactive, which is one of the key metrics related to how users perceive your page speed.
  • Memory cost: If your JavaScript holds on to a lot of references, it can potentially consume a lot of memory. Pages appear janky or slow when they consume a lot of memory. Memory leaks can cause your page to freeze up completely.

Technically, as you do your computed style calculation, Lighthouse shows a warning when JavaScript execution takes longer than 2 seconds. The audit fails when execution takes longer than 3.5 seconds:

A screenshot of the Lighthouse Reduce JavaScript execution time audit

To help you identify the biggest contributors to execution time, Lighthouse reports the time spent executing, evaluating, and parsing each JavaScript file that your page loads. With that in mind, make sure that you have a look at the Lighthouse Performance Scoring Post to learn more about how your website page’s overall performance score is calculated.

The process of calculating styles for the elements is broken into 3 phases:
  • Gathering, partitioning, and indexing the style rules present in all of the style sheets
  • Visiting each element and finding all of the rules which apply to that element
  • Combining those rules and other information to produce the final computed style
The following are long-lived objects that remain static during the calculation of each element’s style.
  • Element
  • TreeScope Represents a tree of elements for a document or shadow root, to give fast access to various things inside.
  • The tee element holds a ScopedStyleResolver for this scope.
  • StyleEngine
  • StyleResolver
  • ScopedStyleResolver
  • TreeScopeStyleSheetCollection
  • StyleRule
  • RuleData
  • RuleSet
The following are short-lived objects that are used when computing a single element’s style.
  • ElementResolveContext
  • StyleResolverState
  • MatchRequest
  • ElementRuleCollector
  • SelectorCheckingContext
  • SelectorChecker

You can have a look at dom/README.md which has more detail on the above topic titles. In this way, Blink works through various lists of RuleData for the element calling CollectMatchingRulesForList on each list. Moreover, how that works is described (for style calculation md as such) in more detail.  That said, below are the other key methods to consider:

1. Reduce the number of elements being styled 

To begin with, one great performance consideration, which is typically the more important factor for many style updates, is the sheer volume of work that needs to be carried out when an element changes. In general terms, the worst-case cost of calculating the computed style of elements is the number of elements multiplied by the selector count.

Obviously, because each element needs to be at least checked once against every style to see if it matches. It used to be the case that if you changed a class on — say — the body element, all the children on the page would need to have their computed styles recalculated. Thankfully, that is no longer the case; some browsers instead maintain a small collection of rules.

Specifically, rules that are unique to each element that, if changed, cause the element’s styles to be recalculated. This means, that an element may or may not need to be recalculated depending on where it is in the tree, and what specifically got changed. Style calculations can often be targeted to a few elements directly rather than invalidating the page as a whole.

2. Reduce the complexity of your selectors

And now, after you learn how to reduce the number of elements being styled, the next item on the list is to reduce the complexity of your selectors as well.  In the simplest case you reference an element in your CSS with just a class:

.title {
    /* styles */
}

But, as any project grows, it will likely result in more complex CSS. And you may end up with selectors that look like this:

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

In order to know what styles need to apply the browser has to effectively ask “is this an element with a class of title which has a parent who happens to be the minus nth child plus 1 element with a class of box?” However, figuring all this out can take a lot of time, depending on the selector used and the browser in question (but it could be changed to a class instead).

Such as follows:
.final-box-title {
    /* styles */
}

You can take issue with the name of the class, but the job just got a lot simpler for the browser. In the previous version, in order to know, for example, that the element is the last of its type, the browser must first know all about the other elements.

And whether they are any elements that come after it that would be the nth last child. After all, this is something that is potentially a lot more expensive than simply matching up the selector to the element because its class matches.

3. Use the computed style to measure the recalculation cost

The easiest/best way to measure the cost of style recalculations is to use Chrome DevTools’ Timeline mode. Just open DevTools, go to the Timeline tab, hit record, and interact with your website. When you stop recording you’ll see something like this:

DevTools showing long-running style calculations.

Clearly, as you can see, the strip at the top indicates frames per second, and if you see bars going above the lower line, the 60fps line, then you have long-running frames.

Zooming in on a trouble area in Chrome DevTools.

If you have a long-running frame during some interaction like scrolling, or some other interaction, then it bears further scrutiny. If you have a large purple block, as in the case above, click the record to get more details.

Getting the details of long-running style calculations.

In this grab, there is a long-running Recalculate Style event that is taking just over 18ms, and it happens to be taking place during a scroll, causing a noticeable judder in the experience. If you click the event itself you are given a call stack, which pinpoints the place in your JavaScript that is responsible for triggering the style change.

In addition, you also get the number of elements that have been affected by the change (in this case just over 400 elements), and how long it took to perform the style calculations. You can use this information to start trying to find a fix in your code.

4. Use the block, element, and modifier tools

Approaches to coding like BEM (Block, Element, Modifier) actually bake in the selector matching performance. Because it recommends that everything has a single class, and, where you need hierarchy, that gets baked into the class name like:

.list { }
.list__list-item { }

If you need some modifier, like in the above where we want to do something special for the last child, you can add that like so:

.list__list-item--last-child {}

If you’re looking for a good way to organize your CSS, BEM is a really good starting point, both from a structure point-of-view, but also because of the simplifications of style lookup. If you don’t like BEM, there are other ways to approach your CSS, but the performance considerations should be assessed alongside the ergonomics of the approach.

5. Minimize your overall web browser reflow

To enumerate, web browser reflow is the name of the process for re-calculating the positions and geometries of elements in the document, for the purpose of re-rendering part or all of the document. Because reflow is a user-blocking operation in the browser, it is useful for developers to understand how to improve reflow time in general terms.

As well as to also understand the effects of various document properties (DOM depth, CSS rule efficiency, different types of style changes) on reflow time. Sometimes reflowing a single element in the document may require reflowing its parent elements and also any elements which follow it.

There are a great variety of user actions and possible DHTML changes that can trigger a reflow. Resizing the browser window, using JavaScript methods involving computed styles, adding or removing elements from the DOM, and changing an element’s classes are a few of the things that can trigger reflow.

It’s also worth noting that some operations may cause more reflow time than you might have imagined — there are some easy guidelines to help you minimize reflow in your web pages.

Consider some of the following:
  • Reduce unnecessary DOM depth. Changes at one level in the DOM tree can cause changes at every level of the tree — all the way up to the root, and all the way down to the children of the modified node. This leads to more time being spent performing reflow.
  • Minimize CSS rules, and remove unused CSS rules.
  • If you make complex rendering changes such as animations, do so out of the flow. Use position-absolute or position-fixed to accomplish this.
  • Avoid unnecessary complex CSS selectors — descendant selectors in particular — which require more CPU power to do selector matching.

As a matter of fact, even at Web Tech Experts, we test the speed of our web pages and applications in a variety of ways — and reflow is a key factor we consider when adding features to our UIs. And, by all means, we strive to deliver a lively, interactive, and delightful User Experience (UX) to all our target audience and potential website users like yourself.

More ways to speed up time:

  1. Minify and compress your code
  2. Try to remove any unused code
  3. Only send the code that your users need by implementing code splitting
  4. Reduce network trips by caching your code with the PRPL pattern
  5. Source code for Reduce JavaScript execution time audit

For other ways to improve page load, check out the Performance Audits Landing Page which has more detail. You can also always Contact Us at any time if you’ll need more help in improving your overall website performance speed.

Summary Notes:

Since we split simple selectors from the rightmost compound selectors into distinct members of the invalidation sets, combinations of type selectors and classes/ids can be sub-optimal. Consider the selector “.a span.b”. Say you have a div with a large number of span descendants, only one of which has the class “b”, and set the class a on that div.

Only the span with class “b” needs a style recalculation. But, since the invalidation set for “.a” becomes “{ span, .b }”, we will recalculate the computed style for all those span descendants. Adjacent combinators are still very expensive. It is possible to extend the invalidation set concept to add Adjacent Invalidation Sets. And, there are several ways to implement that.

Be that as it may, although Descendant Invalidation Sets will give you false positives when marking elements for style recalculation, it has proven to reduce the number of elements affected during style recalculation drastically, and hence reduced occurrences and severity of janks. That’s it! Kindly share your additional thoughts or contribution questions down below.


Trending Content Tags:


Please, help us spread the word!

2 Comments

Comments are closed.