StackExchange / StackExchange/Stacks

Using universal selector (`*`) on custom property definitions causes all style recalculations on `stackoverflow.com` to take at least twice as long

Open
#1,842 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
672
Forks
107
Avg merge
2d 12h
Merged PRs (30d)
26

Description

This bug report is about a performance issue in the implementation on stackoverflow.com, not about incorrect behavior. However it has a huge impact and is also easy to address with a simple change. I first considered using SO's internal feedback system, but found this repo to be a much better place for this kind of problem.

The problem

Due to how browsers (at least Chromium) process custom properties, using the universal selector when defining them on the html or body element slows down every recalculation by a factor of at least 2. On some complex pages I found it to make it take almost 4 times longer (~240ms vs ~60ms).

Consider the following performance recording of the home page:
SOslow

Observe that, by far, most time is spent on rendering. In fact you're waiting much, much longer on rendering than on the network requests. Clearly this is the main bottleneck and any improvement to it directly translates to a faster page load. For all SO pages I tested, the vast majority of rendering tasks are style recalculations (e.g. the final large purple bar in the recording is a single style recalculation taking around 200ms).

I found that these recalculation times can be slashed in (at least) half, just by changing the following selector in primary.css

body.unified-theme, body.unified-theme * {
  --theme-base-primary-color-h: 27;
  // ...

to be only

body.unified-theme {
  --theme-base-primary-color-h: 27;
  // ...

On the StackExchange GitHub org I found a source sheet with the same pattern.

Removing the universal selector does not change the visual result, because every element still inherits the values from body. From my own experience I can assure that this is a change you can safely make, and I also confirmed on live pages that the second part of the rule is truly not needed.

Why is this so slow?

While I'm not fully sure about it, it's safe to say that using * to define custom properties puts the browser into a case that it's not optimized for at the moment. I guess it's not optimized as a pattern because it's not needed, since inheritance is exactly made to do this and enabled by default on custom properties.

While there's a chance that the slow performance is an oversight on the browser's end, if the pattern is truly unnecessary here then removing it seems the logical thing to do.

Expected improvement

As a simple POC, I downloaded a random answer page stripped of all scripts. I verified that the local copy had similar recalculation times as the original live page.

I then made another local copy of that page where the only difference is the aforementioned rule change in primary.css.

Here's a screenshot of the recordings side by side.

SObeforeafter

With the fix, time spent rendering went from 326ms to 194ms. A bit harder to see in the screenshots, every individual recalculation in this case was about 3 times faster (first one went from 94ms to 28ms).

That's over 100ms sliced off, and on the live page, which has more recalculations triggered by scripts, the improvement will be bigger, probably several hundreds ms on a desktop.

On lower end devices, I expect this improvement to be much bigger, even 1 second or more on big pages.
You can imagine how such an improvement will reflect in metrics like bounce rate.

Verifying this improves performance on every page

The simplest way to verify this is:

  • trigger a recalculation manually on any SO page and record using dev tools
    You can run following snippet in the console right before pressing the record button:
setTimeout(() => {
    document.documentElement.style.setProperty('--theme-button-color', `blue`)
}, 3000)
  • reload the page so the snippet doesn't set the same value twice (you can also swap out the color to avoid reloading)
  • use dev tools to remove the second part of the rule as mentioned above (inspect any element to find this rule, it's a large list of striked through custom properties repeated for every parent element of any element)
  • run the snippet again and record
  • compare recalculation times in the recordings

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in primary.css and inspect the rule beginning with body.unified-theme, body.unified-theme *. Use the provided DevTools snippet to trigger a style recalculation, remove the universal-selector portion, and compare recordings; done means the rule only targets body.unified-theme without changing the visual result and with improved recalculation time.

Written by the indexing model from the issue text.

Assessment

Tech stack
css
Domain
frontend, performance
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.