-
Notifications
You must be signed in to change notification settings - Fork 11
perf: optimize DOM operations for better web vitals (INP, CLS, FID) #657
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…web vitals - Batch reflow-causing reads in isFocusable() to minimize layout thrashing - Use Set for O(1) tag lookups instead of Array.includes() - Optimize getClippingRect() and getPositionedParent() with early exits - Batch MutationObserver DOM operations (read phase, then write phase) - Add sr-only inline styles to focus-trap sentinels to prevent CLS - Cache isMacOS() result to avoid repeated userAgent parsing - Add passive: true to mousemove listener in focus-zone - Use :scope selector for faster direct-child sentinel lookup - Replace for...of with indexed for loop in querySelectorAll iteration
🦋 Changeset detectedLatest commit: 581c0b4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR aims to optimize DOM operations across the behaviors library to improve Core Web Vitals metrics (INP, CLS, FID) by batching reflow-causing reads, using more efficient data structures and selectors, and preventing layout shifts.
Key changes:
- Refactored
isFocusable()to batch layout-triggering property reads and reorder checks for early exits - Implemented batched DOM operations in MutationObserver callbacks to separate read and write phases
- Added inline sr-only styles to focus trap sentinels to prevent CLS
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/utils/iterate-focusable-elements.ts |
Optimizes isFocusable() by using a Set for tag lookups, batching reflow-causing reads, and reordering checks for early exits |
src/focus-zone.ts |
Caches isMacOS() result, refactors MutationObserver to batch DOM operations in read/write phases, and uses indexed for loops |
src/focus-trap.ts |
Adds inline sr-only styles to sentinels for CLS prevention and optimizes sentinel detection with :scope selector |
src/anchored-position.ts |
Adds early exit conditions to getPositionedParent() and getClippingRect() to avoid unnecessary iterations |
.changeset/web-vitals-optimizations.md |
Documents the performance optimizations for the changelog |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ogic - Fixed isFocusable() to not exclude position:fixed/sticky elements (offsetParent is null for these) - Fixed MutationObserver to check current attribute state instead of relying on oldValue - Use Sets for deduplication in MutationObserver to avoid processing same element twice - Added tests for fixed/sticky positioned elements
This PR optimizes DOM operations across the behaviors library to improve Core Web Vitals metrics, particularly Interaction to Next Paint (INP), Cumulative Layout Shift (CLS), and First Input Delay (FID).
Changes & Expected Impact
1.
isFocusable()- Reduced Layout ThrashingFile:
src/utils/iterate-focusable-elements.tsChanges:
hidden,classList,instanceof)offsetWidth,offsetHeight,offsetParent) together before conditional logicArray.includes()withSet.has()for O(1) tag lookupsgetComputedStyle()andgetClientRects()calls to run only after cheaper checks passposition: fixedandposition: stickyelements (these haveoffsetParent === nullbut are still visible)2.
getClippingRect()/getPositionedParent()- Optimized DOM TraversalFile:
src/anchored-position.tsChanges:
document.bodyis reached3. MutationObserver Callback - Batched DOM Operations
File:
src/focus-zone.tsChanges:
flatMap+ spread operators with direct array push loops to eliminate intermediate array allocationshasAttribute()instead of relying onmutation.oldValueSetfor automatic deduplication of elements in multiple mutationsobserver.disconnect()on abort to prevent memory leak4. Focus Trap Sentinels - CLS Prevention
File:
src/focus-trap.tsChanges:
position:absolute;clip:rect(0,0,0,0);...)className,tabIndex) instead ofsetAttribute()Array.from().filter()with:scope > span.sentinelselector for faster lookup5. IndexedSet Data Structure - O(1) Membership Checks
File:
src/utils/indexed-set.ts(NEW),src/focus-zone.ts.has())Changes:
IndexedSet<T>class combining array (ordering) + Set (O(1) lookup)focusableElementsfromHTMLElement[]toIndexedSet<HTMLElement>.has()for O(1) membership checks.get(i)and.sizeproperty6. Event Listener & Caching Optimizations
File:
src/focus-zone.tsChanges:
isMacOS()result to avoid repeated userAgent regex parsing on every keydown eventkey.lengthvs[...key].length)Testing
position: fixedelements are focusable in strict modeposition: stickyelements are focusable in strict modeWeb Vitals Impact Summary
Memory Optimizations
savedTabIndex- allows GC of removed elementsBug Fixes Included
position: fixedorstickyare now correctly identified as focusablehidden/disabledattributes are added vs removedChecklist