v6.0.0-alpha.1: :has() control-state rules cause multi-second style recalculation on pages with many form controls (switch/check/radio/range/form-field)

Open
#43,003 2 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
15/100
Issue type
Bug
Clarity
Needs clarification
Activity status
Active
Tech stack
bootstrap, css

Research direction

The report names the :has() control-state selectors in bootstrap.css, such as :has(input:checked), :has(input:disabled) and :has(.check:checked), and says the cost appears when a theme variable or the html attribute changes. Start by building a repro that reproduces the slowdown, since the reporter's minimal page did not, by varying control count, nesting and checked/disabled state. Done means a reproduction that shows the cost and a measured explanation of which rules cause it.

Written by the indexing model from the issue text.

Description

bug css performance utility-api v6

Summary

On a real admin page that uses Bootstrap 6.0.0-alpha.1, changing one attribute on takes about 12 seconds before the next frame is painted. Removing only the Bootstrap rules that use :has() on form controls brings the same change down to about 1.9 seconds. Removing our own :has() rules changes nothing.

I have not yet reproduced this on a minimal page that uses only Bootstrap's stylesheet, so I am reporting it as a measured regression on a real page, with the page-level evidence below. I would welcome guidance on how to build a smaller repro.

Environment

Bootstrap: 6.0.0-alpha.1 (npm, [email protected])
Browser: Google Chrome (stable), Windows 11
Page: an admin dashboard with a sidebar, a topbar, a customizer panel (a drawer with radio inputs, switches, checkboxes and a range), and several cards containing form fields and tables
Stylesheet size: the compiled admin CSS contains about 130 :has() selectors, about 107 of them from Bootstrap

Steps to reproduce (on our page)

Open the page.
Open the customizer and click a different accent swatch (this changes data-accent on , which changes a set of custom properties used across the page).
Measure the time from the click to the next painted frame (two requestAnimationFrame callbacks after the click).

Measured results

Time from click to next painted frame, one accent change, 1440x900, median of repeated runs:

Condition Click-to-paint
Unmodified page about 12,800 ms
Only our own :has() rules removed (61 rules) about 12,850 ms
Only Bootstrap's :has() rules removed (107 rules) about 1,940 ms
Other measurements from the same page:

Removing only the validation-state rules (.is-invalid, .is-valid, :user-invalid, :user-valid): no change (about 12,600 ms).
Removing only the rules for switch, check, radio, range, form-field, form-adorn, chip-input and form-ghost: about 2,050 ms.
Page load to DOMContentLoaded on the same page: about 9 s (Bootstrap 5.3.8 on the same page: about 1.8 s).

Observations

The cost comes from a few hundred :has() selectors that test form-control state (for example :has(input:checked), :has(input:disabled), :has(.check:checked)) on ancestors such as form fields and switch wrappers.
Every time the accent changes, many custom properties change. Each element with a matching :has() rule then has to be re-checked, which is where the time goes.
The same family is present in Bootstrap 5.3.8 in a different form, but on this page 5.3.8 does not show the delay.

What I tried to reproduce it minimally, and what happened

I built a page with 2,000 .form-field > .check checkboxes, loaded only bootstrap.css from 6.0.0-alpha.1, and measured a click that changes an attribute on and a click that changes --primary-bg on . The click-to-paint time was about 6 to 13 ms for the attribute change and about 110 to 125 ms for the variable change, with and without the :has() rules. So the minimal page does not show the regression. The difference must come from something the minimal page lacks: the number and nesting of controls, the :checked / :disabled state of the matched inputs, or the variables that change. I have not identified which, and I'd value the maintainers' view.

Expected

Changing a theme variable or an attribute on should not take seconds on a page with a few thousand form controls.

Actual

About 12 seconds to the next painted frame on our page, reduced to about 2 seconds when Bootstrap's control-state :has() rules are not present.

Notes

I can share the page and the measuring script on request.
We are considering changing our own markup to set state classes from JavaScript, so this may be a non-issue for us in the long run, but the Bootstrap rules remain in the stylesheet for everyone else.

Dominant language
MDX
Stars
175k
Forks
78.7k
Avg merge
1d 7h
Merged PRs (30d)
103

Getting set up

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.

More from twbs/bootstrap

All issues in twbs/bootstrap

Similar issues

More Web Dev issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.