Skip to content

Defer --dialog-scrollgutter computation to avoid synchronous reflow on page load#3950

Merged
jonrohan merged 5 commits intomainfrom
copilot/reapply-dialog-scrollgutter-fix
Feb 28, 2026
Merged

Defer --dialog-scrollgutter computation to avoid synchronous reflow on page load#3950
jonrohan merged 5 commits intomainfrom
copilot/reapply-dialog-scrollgutter-fix

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 25, 2026

Authors: Please fill out this form carefully and completely.

Reviewers: By approving this Pull Request you are approving the code change, as well as its deployment and mitigation plans.
Please read this description carefully. If you feel there is anything unclear or missing, please ask for updates.

What are you trying to accomplish?

DialogHelperElement.connectedCallback eagerly computes --dialog-scrollgutter by reading body.clientWidth — a layout-dependent property that forces a synchronous reflow during page load. This runs for every <dialog-helper> element on the page, negatively impacting Core Web Vitals (LCP, INP).

This PR defers the --dialog-scrollgutter computation to the moment a dialog is actually opened as a modal, avoiding the reflow cost during page load entirely.

Background: PR #3947 attempted to fix this by combining lazy JS computation with a scrollbar-gutter: stable CSS change. PR #3949 reverted that because scrollbar-gutter: stable doesn't work on body (only html) and reserves scrollbar space on non-scrolling pages. This PR re-applies only the JavaScript-side deferral — no CSS changes.

Changes in app/components/primer/dialog_helper.ts:

  • Added a module-level setScrollGutter(doc) helper that computes and sets --dialog-scrollgutter on doc.body.style, using getPropertyValue to skip the computation if the property is already set.
  • Removed the eager --dialog-scrollgutter computation from connectedCallback().
  • Added setScrollGutter() calls just before showModal() in both dialogInvokerButtonHandler and #handleDialogOpenAttribute().

Since --dialog-scrollgutter is only consumed by body:has(dialog:modal.Overlay--disableScroll), it only matters when a dialog is shown as a modal — making lazy computation both safe and correct.

Screenshots

N/A — no visual changes. The scroll gutter behavior is identical; only the timing of when it is computed changes.

Integration

No consuming code changes are required.

List the issues that this change affects.

Closes #3946

Risk Assessment

  • Low risk the change is small, highly observable, and easily rolled back.

What approach did you choose and why?

We defer the body.clientWidth read (which triggers synchronous layout) from connectedCallback (page load) to just before showModal() (user interaction). A simple getPropertyValue check deduplicates the work so it only runs once per document, regardless of how many <dialog-helper> elements exist.

Alternative considered: Using scrollbar-gutter: stable CSS (attempted in #3947, reverted in #3949). This approach was rejected because the property must be applied to html not body, and doing so reserves scrollbar space on all pages including those that don't scroll.

Alternative considered: Using requestAnimationFrame to defer the read. This was rejected because the computation is truly unnecessary until a dialog opens — deferring by one frame still pays the cost during page load.

Anything you want to highlight for special attention from reviewers?

  • The setScrollGutter implementation uses doc.body.style.getPropertyValue('--dialog-scrollgutter') as the deduplication check instead of a WeakSet<Document>. This is simpler and avoids extra module-level state — the style property itself serves as the "already computed" flag.
  • Confirm this correctly handles the case where #handleDialogOpenAttribute() closes and re-opens the dialog (the property will already be set from the first open, so the check short-circuits).

Accessibility

  • No new axe scan violation - This change does not introduce any new axe scan violations.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Lookbook)
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 25, 2026

🦋 Changeset detected

Latest commit: 883ad05

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/view-components Patch

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

…n page load

Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Copilot AI changed the title [WIP] Reapply JavaScript fix for dialog scroll gutter issue Defer --dialog-scrollgutter computation to avoid synchronous reflow on page load Feb 25, 2026
…ScrollGutter

Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
@mattcosta7 mattcosta7 marked this pull request as ready for review February 25, 2026 14:35
@mattcosta7 mattcosta7 requested a review from a team as a code owner February 25, 2026 14:35
Copy link
Copy Markdown
Contributor

Copilot AI left a 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 improves page-load performance by deferring the layout-forcing --dialog-scrollgutter computation from <dialog-helper>’s connectedCallback() to the moment a dialog is actually opened as a modal.

Changes:

  • Introduces a module-level setScrollGutter(doc) helper that computes and sets --dialog-scrollgutter once per document.
  • Removes eager scroll gutter computation from DialogHelperElement.connectedCallback().
  • Invokes setScrollGutter() immediately before showModal() in both click-invocation and [open]-attribute handling paths.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
app/components/primer/dialog_helper.ts Defers scroll gutter computation to dialog-open time via a shared helper.
.changeset/dialog-scrollgutter-lazy.md Adds a patch changeset describing the lazy computation change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -1,3 +1,8 @@
function setScrollGutter(doc: Document) {
if (doc.body.style.getPropertyValue('--dialog-scrollgutter')) return
doc.body.style.setProperty('--dialog-scrollgutter', `${window.innerWidth - doc.body.clientWidth}px`)
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

setScrollGutter takes a Document but uses the global window.innerWidth. If the dialog lives in a different browsing context (e.g. inside an iframe), window.innerWidth may not correspond to doc.body.clientWidth, producing an incorrect gutter value. Use doc.defaultView?.innerWidth (or the dialog’s ownerDocument.defaultView) instead, with a safe fallback if defaultView is null.

Suggested change
doc.body.style.setProperty('--dialog-scrollgutter', `${window.innerWidth - doc.body.clientWidth}px`)
const view = doc.defaultView
const viewportWidth = view?.innerWidth ?? window.innerWidth
doc.body.style.setProperty('--dialog-scrollgutter', `${viewportWidth - doc.body.clientWidth}px`)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this exists already - so not changing it for now, but maybe this makes sense?

Comment on lines 14 to 18
if (dialogId) {
const dialog = document.getElementById(dialogId)
if (dialog instanceof HTMLDialogElement) {
setScrollGutter(dialog.ownerDocument)
dialog.showModal()
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

This change moves --dialog-scrollgutter initialization from connectedCallback() to just-in-time before showModal(). There are existing system tests for dialogs, but none here assert that opening via <dialog-helper> results in the body scroll gutter being set (even to 0px). Adding a regression assertion in the dialog system test suite would help prevent future reintroductions of eager computation or missing initialization paths.

Copilot uses AI. Check for mistakes.
@mattcosta7 mattcosta7 requested a review from jonrohan February 25, 2026 18:55
@jonrohan jonrohan enabled auto-merge February 28, 2026 06:17
@jonrohan jonrohan added this pull request to the merge queue Feb 28, 2026
Merged via the queue into main with commit ca926de Feb 28, 2026
32 checks passed
@jonrohan jonrohan deleted the copilot/reapply-dialog-scrollgutter-fix branch February 28, 2026 06:30
@primer primer Bot mentioned this pull request Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DialogHelperElement.connectedCallback forces synchronous reflow on page load

4 participants