Performance/Essay/Popover Best Practices (2021)

A popover (sometimes known as popupThe term "pop-up" is historically associated with pop-up ads in online advertising, where distinct browser windows would appear over top other web pages. Within the Wikimedia technical community, the term is strongly associated with what (as of 2020) is more commonly known as "popovers". See also Wikipedia:Tools/Navigation_popups, open-ui issue #627, WHATWG Proposal: Popover API) is a UI element that appears in the foreground, on top of other content. It usually augments the user interface in some way. Popovers are often anchored to another element, for example MediaWiki's Page Previews feature augments a link with a preview of the linked page. Popovers are usually triggered by a user interaction event, such as hovering above a link.
Popovers are common on the web, but there is no standard or native solution for web authors and developers to create them. Most implementations we found fall into performance traps and cause bottlenecks and unintended performance regressions.
In this document, we will attempt to set best practices on how to author popovers in a way that avoids those gotchas and keeps performance efficient over time.
== Terminology ==
For the purpose of this document, we will use the following terminology:
* Target Element: the element that triggered the popover, usually part of the normal document content.
* Popup Element: The element containing the popover UI.
* Placement: Whether the popover is above or below the target.
* Alignment: Whether the popover is aligned to the left or right of the target.
== Performance Gotchas ==
The practices proposed here are meant to avoid several mechanisms that can cause unintended performance bottlenecks:
* Layout/style thrashing: style calculations/layouts that occur synchronously, not for the purpose of painting. see developers.google.com's documentation for an explanation.
* Redundant HTML parsing
* Redundant JavaScript
== Creeping Normality vs. Premature Optimizations ==
Note that when looking at each optimization in isolation, they wouldn't look significant, and it would be hard to pin-point them in a performance trace. However, beware of creeping normality, a situation where many small inefficient code-paths are accumulated on top of each other, causing slowdowns that are difficult to debug and fix.
It's not easy to find the balance between avoiding premature optimizations and avoiding creeping normality, but staying aware of best practices can help.
== Best practices ==
=== Positioning the Popup ===
To position the popup, we always need a root element. The root element is roughly equivalent to the target element.
When we have the root element, we can position the popup with the following CSS:
.root > .popup[placement="above"] { bottom: 100% }
.root > .popup[placement="below"] { top: 100% }
.root > .popup[alignment="left"] { right: 100% }
.root > .popup[alignment="right"] { left: 100% }
Note that for the above to work, the root needs to have relative or absolute positioning.
=== Positioning the root element ===
There are two ways to position the root element, manually and automatically.
For simple popups, the root element can be the target element itself, or be a child of the target element.When possible, use automatic positioning - make the root element a descendant of the target element and position it in a normal way.
But if either of the following occurs, the root element has to be positioned manually:
* The target element is multi-line, and the popup has to be positioned according to only one of the lines (e.g. the hovered line).
* The target element is part of an overflow/stacking context, which the popup should not adhere to. For example, the content has a z-index which puts it behind navigation elements, and the popup has to appear on top of those elements.
If manual positioning is necessary, do the following:Measure everything you need synchronously at the event listener (e.g. onmouseover), before any DOM changes occur and before any timeouts and asynchronous operations such as Redux dispatching.The above will help avoid layout/style thrashing: if the measurements are postponed, changes in the DOM can occur before them, causing unnecessary calculations of style and layout. Also, having a single "measurement" phase keeps the code organized, and makes it necessary to find CSS layout solutions to positioning problems (which usually exist).
Once all the measurements, including calls to getComputedStyle and retrieving offsets from the event are done, DOM manipulations can be called freely, without need for batching - the browser will anyway batch them before the next paint, as long as there are no hidden measuring of style or layout.
=== Determine placement Early ===
If possible, determine the placement straight away after measurement, by using the offsets gathered from the event and the measurements performed in the previous phase.
The size of the viewport, the mouse event coordinates (or the measurements in case of a keyboard event) should suffice to determine the placement of the popup.
=== Make layout computations in CSS ===
Most, if not all, of pixel computations can be done inside CSS. CSS is a lot faster to parse and execute, and changes in CSS are guaranteed to not generate thrashing.
Today with new CSS functions such as max() and CSS custom properties, there is very little need to perform JavaScript math for determining positioning and margins.
=== Use template element instead of re-parsing the same HTML ===
To avoid expensive and unnecessary HTML parsing and parameter normalization, create the DOM necessary for the popup in advance, kept in a template element. When the measurements and contents are known, clone the template content, fill in the attributes, class names, slots and CSS variables, and after everything is in place, add the cloned node to the DOM.
This has the added side-effect of keeping tidy HTML files for the popup, instead of injecting HTML into JavaScript.
=== Use CSS clip-path for clipping the popup shape ===
The new-ish clip-path attribute is widely supported, and can be used to clip the popup with complex shapes, like balloons.
Note that clip-path also clips shadow filters, so it's advised to put the clip-path on the popup element and the filters on the root element.
=== Rendering Steps ===
If possible, the following steps should be sufficient:
* Make necessary measurements and read event properties (offsetX, clientX etc) when the event arrives
* If using manual placement, determine the placement and alignment according to the event, measurements and viewport
* Fetch the content necessary for the popup
* Form the popup by cloning a pre-existing template
* Set the raw measurements and content of the popup in the form of classes, attributes, inner HTML (only for the dynamic part!) and CSS custom properties.
* Let CSS do the rest
Category:Wikimedia Performance