Dialog showModal() causes unnecessary full-document layout

#41312488 Filed 2017-04-11 Reporter: ash@scirra.com P4 / S5 / Feature

Original report

On editor.construct.net, calling dialog.showModal() produces a ~22 ms layout on a high-end desktop. The reporter argues that a modal floating on top of the document should not require relaying out the whole document.

2020 follow-up by ikilpatrick@chromium.org: the original 22 ms appears to be partly explained by a synchronous font-fallback IPC at first dialog display, not pure layout cost.

Live reproducer

Builds a 40×40 CSS-grid (1600 cells) and times showModal() + close() across N iterations. Each iteration also reads a layout property on the body (offsetHeight) to force a synchronous layout flush, so we can attribute any cost back to showModal/close.

Open DevTools → Performance, click Run 10x, and look for layout work synchronously parented under the showModal call. If full-document layout is still being triggered, "Layout" will appear with the whole document scope on each iteration.

About

Modal dialog content (just like the original Construct repro).

Results

#showModal (ms)close (ms)forced reflow (ms)total (ms)

Measured behavior (Chrome Canary, 50 runs)

Steady state (after iteration 0):

The 0.00 ms forced reflow is the key signal: nothing is being deferred. The whole cost is paid synchronously inside the API calls. Iteration 0 is ~13 ms (extra ~7 ms), consistent with a one-time sync font-fallback IPC on first dialog display (separate bug 1104538).

Root cause (Blink, today)

HTMLDialogElement::showModal in third_party/blink/renderer/core/html/html_dialog_element.cc does:

InertSubtreesChanged(document, old_modal_dialog);
document.UpdateStyleAndLayout(DocumentUpdateReason::kJavaScript);

That explicit UpdateStyleAndLayout was added in ab14f17b660d0 (2021-11, Brufau): “[inert] Track inertness in ComputedStyle”. Since that change, inert is an inherited ComputedStyle property, so when InertSubtreesChanged marks documentElement dirty, every element in the tree must have its ComputedStyle recomputed to flip its inert bit. The follow-on layout pass is then also document-wide.

close() doesn't call UpdateStyleAndLayout explicitly, but it calls InertSubtreesChanged and then previously_focused_element->Focus(), which itself flushes style+layout. Same shape, same cost.

UA stylesheet rules on :modal and :-internal-dialog-in-top-layer only target the dialog element, so they are not the issue. Author-side :has(dialog:modal) rules would compound the cost but aren't required to reproduce it.

Why a quick patch is risky

Removing the explicit UpdateStyleAndLayout in showModal would break correctness: SetFocusForDialog -> GetFocusDelegate walks the dialog subtree and reads IsInert() / IsFocusable(), both of which depend on the updated ComputedStyle. Without the flush the wrong element could be focused.

The real fix has to attack the cost itself:

Either approach partially refines the 2021 inert-in-ComputedStyle architecture and would need sign-off from futhark@ / obrufau@.

References

Issue: crbug 41312488 (migrated from 710523)

Font-fallback follow-up: crbug 1104538

Source: html_dialog_element.cc