Skip to content

Conversation

@cacieprins
Copy link
Contributor

@cacieprins cacieprins commented Oct 22, 2025

Additional details

This is a DRAFT proposal

In order to verify performance improvements, this adds a "stress test" e2e test to the driver that simulates interacting with extremely large virtual scrolling lists. This test should probably not be run with "legacy" visibility, as it immediately causes the browser to eat up all available memory and crash. This fixture works perfectly fine outside of Cypress.

This introduces an experimentalFastVisibility option, that switches our visibility detections to an alternative algorithm. This algorithm has some caveats, but it is much faster than the current visibility algorithm.

Cypress uses this visibility algorithm not just for visibility assertions, but also for every interaction that requires "interactability." It's memory intensive and can cause unnecessary layout thrashing due to repeated access of CSS properties that require layout recaulcation.

The "fast" visibility algorithm:

  1. Assumes body and html are always visible; this is in line with current visibility behavior
  2. Uses the built-in checkVisibility method as first pass, with all options enabled. The following states are considered hidden by this method:
  • The element does not have an associated box
  • The element is not being rendered because it or one of its ancestors has set content-visibility to hidden
  • It has an opacity of 0
  • The value of its visibility property makes it invisible
  • content-visibility CSS value is auto, and its derived value prevents the element from being rendered
  1. If the element is an <option> or <optgroup>, it defers to the visibility of the parent <select> element. If there is no parent <select> element (an invalid DOM tree state), the element is considered hidden.
  2. If the element is still considered visible, it performs a more comprehensive check with an adaptive point sampling algorithm

The point sampling algorithm:

  1. Implements a visibleAtPoint check, which uses document.elementFromPoint to determine which element is at the top of the render context at that point. If the element at that point is the subject element or a child of the subject element, the subject element is considered visible at that point.
  2. From the bounding rectangle of the subject element, it checks the four corners and center. If any of these points are visible, the subject element is considered visible.
  3. If none of these points are visible, it divides the bounding rectangle in to four sub-rectangles, and performs the same sampling on them.
  4. The sampling algorithm returns true if any part of the element is visible, but for performance constraints it will only subdivide to a limited depth, and will stop subdividing if the sampling rectangles fall below 1px in both dimensions.

Benefits over the legacy algorithm:

  • Constant-time in the best case, and bounded exponential in the worst case (point sampling on a fully hidden element)
  • Detects when an element is fully covered by positioned elements outside its ancestor tree
  • Can be tweaked to provide access to threshold-based visibility
  • Could be extended to require a minimum visible size for an element to be considered actionable tap events, to test conformation with UX guidelines regarding the size of touch targets
  • Does not crash when running the virtual scroll stress tests

Caveats:

  • Elements that are outside the bounds of the browser viewport will always be considered hidden with this algorithm
  • Elements with pointer-events:none either explicit or inherited will always be considered hidden
  • <option> elements that are not a direct child of <select> or <optgroup> elements are not considered visible
  • <optgroup> elements that are not a direct child of <select> or <optgroup> elements are not considered visible
  • Certain other edge cases that are considered hidden to this algorithm may be visible to the user, especially when it comes to elements that have a 0 height or width and have visible children. In these cases, it may be better to assert visibility on the visible children rather than the containing element.
  • Shadow dom support is unknown at this time
  • Some of the current visibility.cy.ts tests fail because the subject element is either off-screen, or covered by an absolute/fixed element that is not an ancestor.
  • If even 1px of the subject element is visible, it will be considered visible. Potential consideration: configurable threshold?

Future performance improvements:

  • Memoize elementFromPoint calls
  • delete old records from memoization maps to reduce memory footprint
  • Use requestAnimationFrame to measure the BoundingClientRect of the subject element; this is difficult due to how the visibility & interactability checks are currently wired through jquery selectors, which prevents this method from being async

Steps to test

How has the user experience changed?

PR Tasks

@cypress
Copy link

cypress bot commented Oct 23, 2025

cypress    Run #67940

Run Properties:  status check passed Passed #67940  •  git commit b5ba8885ef: add i18n copy for experimentalFastVisibility, documented as a step to take in cf...
Project cypress
Branch Review visibility-performance
Run status status check passed Passed #67940
Run duration 17m 34s
Commit git commit b5ba8885ef: add i18n copy for experimentalFastVisibility, documented as a step to take in cf...
Committer Cacie Prins
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 2
Tests that did not run due to a developer annotating a test with .skip  Pending 10
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 640
View all changes introduced in this branch ↗︎
UI Coverage  0%
  Untested elements 4  
  Tested elements 0  
Accessibility  100%
  Failed rules  0 critical   0 serious   0 moderate   0 minor
  Failed elements 0  

@cacieprins cacieprins force-pushed the visibility-performance branch from ac255b8 to f8b1b6f Compare December 10, 2025 14:44
@cacieprins cacieprins marked this pull request as ready for review December 10, 2025 17:18
@jennifer-shehane jennifer-shehane self-requested a review December 10, 2025 18:17
Comment on lines +19 to +21
**Performance:**

- Introduced a new `experimentalFastVisibility` experiment. Enabling this experiment changes how Cypress performs visibility checks and assertions. Read more about [experimental fast visibility](https://on.cypress.io/experiments/#experimental-fast-visibility). Addresses [#33044](https://github.com/cypress-io/cypress/issues/33044). Addressed in [#32801](https://github.com/cypress-io/cypress/pull/32801).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There used to be some guidance on this, that I cannot find atm. But we put Performance at the top, above Features (since they usually impact generally everyone).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@jennifer-shehane jennifer-shehane left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cacieprins Some notes to possibly address

Comment on lines +130 to +132
if (isBody(subject) || isHTML(subject)) {
return false
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems this could be moved out of both functions and ad the top of the isHidden definition. Not a big deal though

Comment on lines +320 to +321
// Chromium consider these elements visible, but Firefox considers them hidden. Should defer to browser
// behavior, maybe?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the decision here. Comment makes it sound undecided

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decision should be to defer to the browser behavior based on what we discussed. Based on the logic it looks like that is what is happening and the comment just needs to be cleaned up?

@@ -0,0 +1,330 @@
# Fast Visibility Algorithm Migration Guide

_(note: this file or content similar to it will be added to cy docs, when language / details are finalized)_
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove this note

Comment on lines +12 to +13
- **Significantly faster** visibility calculations for complex DOM structures
- **Reduced CPU usage** during test execution
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might mention here that one would get benefit even if they are not explicitly doing a lot of 'visible' assertions.

Comment on lines +67 to +71
module.exports = {
e2e: {
experimentalFastVisibility: true
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only available in e2e? I think it's a global setting right?

Run your existing test suite to identify any failures:

```bash
npm run test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
npm run test
npm run cypress run

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this be npx cypress run @jennifer-shehane ?

This allows you to gradually migrate specs while keeping failing ones working.

### Step 5: Fix Tests with Visibility-Related Failures
For specs that fail due to visibility algorithm differences, update the test expectations to match the correct behavior (see solutions below).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might mention: correct behavior = visually look at it and see if it is visible or not!

Comment on lines +212 to +216
### Issue 7: Shadow DOM incompatibilities

**Problem:**: Elements inside shadow DOMs may not be detected properly as visible or hidden.

**Solution:**: Test shadow dom components in isolation with component testing, and only test if the public interface of the shadow dom component is visible. You wouldn't assert on the visibility of the browser's default video play controls by querying its shadow dom: you would assert on the properties of the video element itself.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the result of passing shadow dom in? Will it give false results of visible or hidden? Can we not error in some way so people don't do this? (Determining if something is shadow dom is not exactly the most performant either probably, so I suspect not)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from my testing, I don't think it will work at all, so if you assert something isn't visible it will always be true and if you assert something is visible it will always be false. Is that correct, @cacieprins ?

Comment on lines +226 to +228
e2e: {
experimentalFastVisibility: false // Disable fast visibility
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again specifies as e2e

- **Some compatibility differences exist** - when tests fail, the fast algorithm is likely correct and tests should be updated
- **Performance benefits are significant** - especially for applications with many DOM elements or complex layouts

By following this migration guide, you can resolve compatibility issues and benefit from faster, more accurate visibility detection while understanding the current limitations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected the force: true option to be mentioned in here at some point. I don't like it's use myself, but a lot of people lean back on using it.

Copy link
Member

@jennifer-shehane jennifer-shehane left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add the descriptions for the App UI in Settings.

Screenshot 2025-12-10 at 1 59 44 PM

@AtofStryker AtofStryker self-requested a review December 11, 2025 21:04
Comment on lines +19 to +21
**Performance:**

- Introduced a new `experimentalFastVisibility` experiment. Enabling this experiment changes how Cypress performs visibility checks and assertions. Read more about [experimental fast visibility](https://on.cypress.io/experiments/#experimental-fast-visibility). Addresses [#33044](https://github.com/cypress-io/cypress/issues/33044). Addressed in [#32801](https://github.com/cypress-io/cypress/pull/32801).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +320 to +321
// Chromium consider these elements visible, but Firefox considers them hidden. Should defer to browser
// behavior, maybe?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decision should be to defer to the browser behavior based on what we discussed. Based on the logic it looks like that is what is happening and the comment just needs to be cleaned up?

this.attachShadow({ mode: 'open' })
this.style.display = 'block'
}
// #TODO: support shadow dom in fast visibility algorithm
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a feature issue we can link here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently this doesn't work AT ALL with shadow DOM, right? In other words, asserting any type of visibility on a shadow DOM element will fail?

import $dom from '../../dom'
import $ from 'jquery'

export const selectors = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth freezing these in Object.freeze or is that overkill?

Run your existing test suite to identify any failures:

```bash
npm run test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this be npx cypress run @jennifer-shehane ?

| **[overflow-scroll-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element outside clip-path polygon | ✅ Yes | ❌ No | ❌ No | Child element of polygon clip-path parent |
| **[overflow-scroll-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element outside clip-path inset | ✅ Yes | ❌ No | ❌ No | Child element of `clip-path: inset(25% 25% 25% 25%)` |
| **[viewport-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Absolutely positioned element outside of the viewport | ✅ Yes | ❌ No | ❌ No | Elements that are outside of the viewport must be scrolled to before the fast algorithm will consider them visible. This is aligned with scroll-container visibility. |
| **[z-index-coverage](../../../cypress/fixtures/visibility/positioning.html)** | Covered by higher z-index element | ✅ Yes | ❌ No | ❌ No | Element covered by another element with higher z-index |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| **[z-index-coverage](../../../cypress/fixtures/visibility/positioning.html)** | Covered by higher z-index element | ✅ Yes | ❌ No | ❌ No | Element covered by another element with higher z-index |
| **[z-index-coverage](../../../cypress/fixtures/visibility/positioning.html)** | Covered by higher z-index element | ✅ Yes | ❌ No | ❌ No | Element covered by another element with higher z-index |

| **[overflow-scroll-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element outside clip-path inset | ✅ Yes | ❌ No | ❌ No | Child element of `clip-path: inset(25% 25% 25% 25%)` |
| **[viewport-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Absolutely positioned element outside of the viewport | ✅ Yes | ❌ No | ❌ No | Elements that are outside of the viewport must be scrolled to before the fast algorithm will consider them visible. This is aligned with scroll-container visibility. |
| **[z-index-coverage](../../../cypress/fixtures/visibility/positioning.html)** | Covered by higher z-index element | ✅ Yes | ❌ No | ❌ No | Element covered by another element with higher z-index |
| **[clip-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element clipped by CSS clip property | ✅ Yes | ❌ No | ❌ No | Element with `clip: rect(0, 0, 0, 0)` or similar clipping |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| **[clip-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element clipped by CSS clip property | ✅ Yes | ❌ No | ❌ No | Element with `clip: rect(0, 0, 0, 0)` or similar clipping |
| **[clip-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element clipped by CSS clip property | ✅ Yes | ❌ No | ❌ No | Element with `clip: rect(0, 0, 0, 0)` or similar clipping |


### Issue 7: Shadow DOM incompatibilities

**Problem:**: Elements inside shadow DOMs may not be detected properly as visible or hidden.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Problem:**: Elements inside shadow DOMs may not be detected properly as visible or hidden.
**Problem:**: Elements inside shadow DOM may not be detected properly as visible or hidden.


**Problem:**: Elements inside shadow DOMs may not be detected properly as visible or hidden.

**Solution:**: Test shadow dom components in isolation with component testing, and only test if the public interface of the shadow dom component is visible. You wouldn't assert on the visibility of the browser's default video play controls by querying its shadow dom: you would assert on the properties of the video element itself.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Solution:**: Test shadow dom components in isolation with component testing, and only test if the public interface of the shadow dom component is visible. You wouldn't assert on the visibility of the browser's default video play controls by querying its shadow dom: you would assert on the properties of the video element itself.
**Solution:**: Test shadow DOM components in isolation with component testing, and only test if the public interface of the shadow DOM component is visible. You wouldn't assert on the visibility of the browser's default video play controls by querying its shadow dom: you would assert on the properties of the video element itself.

import { unwrap, wrap, isJquery } from '../jquery'
const { isOption, isOptgroup, isBody, isHTML } = $elements

const DEBUG = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guessing this is just a local thing to leverage? can we leverage the debug library to set in the browser? I'm fine with either but maybe a comment

@jennifer-shehane jennifer-shehane self-requested a review December 12, 2025 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Experimental "Fast" Visibility

4 participants