Wikimedia Performance Team/Authoring Popups: Best Practices

Overview
Popups are UI elements that appear in the foreground, on top of the content, usually augmenting the user interface in some way. Many popups are anchored to another element, for example the Preview Popups that augment a link with a preview of the linked page. Popups are usually triggered by a user interaction event, such as hovering above a link.

Popups are very common on the web, but there is no standard solution on how to author them, causing many implementations to fall into performance gotchas that can cause bottlenecks and unintended regressions.

In this document, we will attempt to set best practices on how to author popups in a way that avoids those gotchas and keeps popups performance efficient over time.

Terminology
For the purpose of this document, we will use the following terminology:


 * Target Element: the element that triggered the popup, usually part of the normal document content.
 * Popup Element: The element containing the popup UI
 * Placement: Whether the popup is above or below the target.
 * Alignment: Whether the popup 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 here for 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.

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: 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