Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Explainer: Layout Stability Metric

Overview

Many websites suffer from "layout jank" - DOM elements shifting around due to content loading asynchronously.

We propose a way for the user agent to measure layout jank during a browsing session to compute a cumulative "jank score", which would be exposed by a new interface in the Performance API.

Jank Score

Each animation frame (a.k.a. "browsing context event loop") computes a "jank fraction" approximating the fraction of the viewport affected by layout jank during that frame. An animation frame with no layout jank has a jank fraction of 0.

The jank score is defined as the sum of the jank fractions of each animation frame that has occurred during the browsing session. The jank score is 0 when the page begins loading, and grows whenever jank occurs.

Janking Elements

A "janking element" is one whose visual representation starts in a significantly different location than it did in the previous animation frame. "Starts" refers here to the element's flow-relative offset in the document.

The visual representation of a block-level element is its border box. The visual representation of an inline element is the geometric union of its box fragments, the first of which determines its starting location.

Note that:

  • An element that changes in size (for example, by having children appended), but starts at the same offset, is not a janking element.

  • An element whose start location changes two or more times during the same animation frame (for example, from forced synchronous layouts), but is ultimately painted at the same location as the previous frame, is not a janking element.

  • An element whose start location changes by less than 3 CSS pixels is not a janking element. This allows elements to be animated, as long as the animation is smooth.

Jank Fraction

In general, the "jank fraction" of an animation frame is the fraction of the viewport that is occupied by the geometric union of the previous-frame and current-frame visual representations of all janking elements in that frame.

Illustration of a janking element on a device, with the jank region highlighted

Example: An element which occupies half the viewport moves by a distance equal to half its height. The jank fraction for this animation frame is 0.75.

However, if the user has generated certain UI events within the past 500 ms, the jank fraction is 0. This allows the page to modify its layout in response to the event. The event types that trigger this exception include taps, key presses, and mouse clicks, but not mousemove or events that cause scrolling.

The user agent may trade off precision for efficiency in the computation of jank fractions. It is intended that the jank fraction have a correspondence to the perceptual severity of the jank experience, but not that all user agents produce exactly the same jank scores for a given page.

Iframe and Aggregate Jank Score

The user agent can compute an aggregate jank score of a top-level browsing context by adding the jank score of the top-level browsing context to the weighted jank scores of its descendant browsing contexts, such as those created by <iframe> elements. In performing this aggregation, the jank score of an <iframe> should be weighted by the fraction of the top-level viewport it occupies.

Performance API

Animation frames with non-zero jank fractions will notify a registered PerformanceObserver. The observer's callback receives one or more LayoutJank entries:

interface LayoutJank : PerformanceEntry {
    readonly attribute double fraction;
};

The entry's fraction attribute is the jank fraction. Its entryType attribute is "layoutJank".

The developer can compute the jank score by summing the jank fractions:

addEventListener("load", () => {
  jankScore = performance.getEntriesByType("layoutJank").reduce(
      (score, entry) => { return score + entry.fraction; }, 0);
  new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => { jankScore += entry.fraction; });
  }).observe({entryTypes: ["layoutJank"]});
});

The observer is only notified of jank that occurs after it is registered. The getEntriesByType method retrieves buffered entries for jank that has already occurred. These entries are only buffered until the load event fires. Jank that occurs after the load event can only be seen by the PerformanceObserver.

A "final" jank score for the user's session can be reported by listening to the visibilitychange event, and using the value of jankScore at that time.

A demo page illustrating the use of this code can be viewed in recent Chrome versions with the command-line flag --enable-blink-features=LayoutJankAPI.

Privacy and Security

Layout jank bears an indirect relationship to resource timing, as slow resources could cause intermediate layouts that would not otherwise be performed. Resource timing information can be used by malicious websites for statistical fingerprinting.

The layout jank API only reports jank in the current browsing context (frame). It does not directly provide the aggregate jank score incorporating subframes. Developers can implement such aggregation manually, but browsing contexts with different origins would need to cooperate to share jank scores.

Links

@jesup

This comment has been minimized.

Copy link

jesup commented Nov 28, 2018

I'm concerned that (similar to timing issues related to iframes) that including the layout-jank scores from inside iframes exposes cross-domain information about the contents of the iframe. This will need a privacy/security pass. If iframes have to be excluded, I suspect that doesn't obviate the usefulness of this proposal, though it may limit knowledge of layout jank for iframe-dependent pages.

@gregwhitworth

This comment has been minimized.

Copy link

gregwhitworth commented Dec 21, 2018

Thanks for taking the time to write this up - Todd Reifsteck reached out to me to take a glance at this, sorry for the delay. I don't want to spend too much time on bikeshedding, but I got kind of excited when I read the name layoutJank only to find out that it is a very scoped scenario, one in which there are a few different solutions (CSSWG is looking at aspect-ratio for example) that may help provide web developers with ways to avoid this specific source of jank. So I think, at the very least - renaming it to scrollLayoutJank or something similar would aid in its intuitiveness.

With regards to this statement:

An element that changes in size (for example, by having children appended), but starts at the same offset, is not a janking element.

I'm curious as to why this assumption is made. In many of the perf scenarios I've helped webdevs with, jank is rarely a constant occurrence, which the current verbiage seems to be written with the expectation that it is (based on the defined example and the one linked in the bug, I can understand that this API is indeed defined around one or two very narrow use cases). It is often the case that upon a creation of a very dense DOM does the jank reveal itself due to increased workload of the rest of the pipeline as a result (of which, it may be the cascade at fault and not layout [another reason I'd avoid the term Layout in the name]).

However, if the user has generated certain UI events within the past 500 ms

This seems like a reasonable default, but something that I'd recommend having an option to remove. Many end user interactions that web developers will want to test for jank is going to be following end user interaction outside of scrolling. For example, let's assume twitter wants to add a fancier animation when new items are loaded into the feed? On their desktop site, this is done following the click of "Load new tweets." This is a valid scenario to watch for a jank-free end user experience but currently would not be watched by this observer.

An element whose start location changes by less than 3 CSS pixels is not a janking element. This allows elements to be animated, as long as the animation is smooth.

I assume this is due to wanting some threshold, but it kind of feels like a stab in the dark so I think if this is something that you do want to include in this then you should allow it to be changed by the author.

Personally, I think there is a solid desire from web developers to be able to observe end user jank and being able to understand the role that each aspect of the pipeline played in that jank. However, I think this approach is starting from the wrong end here, which is trying to define jank - and in a very narrow usecase at that. Starting with jank isn't the best approach as it's a subjective metric as one persons jank is another person's smooth. I think what would be much more valuable is a bag of measures that mirrors what web developers are already used to seeing in the DEV tools.

  • SCRIPT
  • DOM
  • STYLE
  • LAYOUT
  • PAINT

The web developer can then utilize this object with the other Performance.* methods to define and react to their own definition of Jank in a given scenario. Sorry for writing a tome :)

@skobes

This comment has been minimized.

Copy link
Owner Author

skobes commented Jan 10, 2019

Thanks for the comments!

The term "jank" may be misleading here, since it's also used to talk about slowness. The goal of this API isn't to detect slowness of rendering stages like script / dom / style / layout / paint. It's only aimed at detecting layout shifts. Perhaps the entryType should be "layoutshift" or "layoutchange".

APIs like aspect-ratio complement this feature by giving developers tools to avoid layout shifts as well as measure them.

I like the idea of configurability for parameters like the input timeout and distance threshold.

The reason we care about offset changes but not size changes is that we don't want to penalize normal incremental loading. For example, consider an entirely static page of text loading over a slow network. The initial layouts will be done on an incomplete page and the <body> element will grow as more content is delivered. But we wouldn't want the expansion of the body element to be treated as layout instability for the purpose of this metric.

@tdresser

This comment has been minimized.

Copy link

tdresser commented Jan 10, 2019

I agree that "Jank" isn't the best term here.
Something like "Layout Stability" is, I think, a better term.

"So I think, at the very least - renaming it to scrollLayoutJank or something similar would aid in its intuitiveness."
This isn't scrolling related - it's about content moving around within the viewport.

"Starting with jank isn't the best approach as it's a subjective metric as one persons jank is another person's smooth."
For layout stability, I don't think this this is the case.
This metric is defined so that for most pages, there should be 0 layout instability.
There are some tricky animated cases that are a bit fuzzy, but the general case is very clear.

@npm1

This comment has been minimized.

Copy link

npm1 commented Jan 14, 2019

From discussion at the call, it seems that the naming was the only objection here. Here are some options for the name:
Prefix: Content, Dom, Layout
Suffix: Instability, Movement, Shift

The score could also be inverted and we could invert the naming correspondingly (for example, LayoutStability). Any thoughts on which of these would be a good name, if any? Other proposals welcome.

@bgirard

This comment has been minimized.

Copy link

bgirard commented Jan 14, 2019

I voiced some concerns in Lyon on the term 'Jank'. Personally I think Layout Instability works best. DOM Instability to me could just be changes to the DOM tree structure of the page that may or not have visible layout implications so I think the Layout prefix is more descriptive.

@paulirish

This comment has been minimized.

Copy link

paulirish commented Jan 14, 2019

I personally prefer "Layout Stability", followed by "Layout instability".

I can't think of a prefix that's reasonable except for "Layout", for reasons like what @bgirard shared.

@npm1

This comment has been minimized.

Copy link

npm1 commented Jan 23, 2019

@gregwhitworth would LayoutStability or LayoutInstability make sense to you?

@gregwhitworth

This comment has been minimized.

Copy link

gregwhitworth commented Feb 1, 2019

Ok, I've been thinking about this more and more and before I jump back into the name debate, I want to call for clarity of the point of this API again.

The reason I ask is due to a statement that was made on the call (I'm paraphrasing):

There is no way to determine the cause of the low score

Based on that statement my issue with this API comes down to one thing, a score has been defined that is not really useful. It will inform you that there is a problem but in no way will it help you solve that problem. That makes me wonder what value this is bringing to web developers?

Switching gears back to the name, we're probably going to have just overload Layout here - which only conflates the problem I denote above because a webdev may receive this negative score and will want to look at CSS or why flex/grid/block, etc is causing issues when in actuality it's the heavy image that took forever to load in the ad network that is above the paragraph. So yes, the layout moved but it's actually due to a large resource coming in after the initial render was possible.

Personally, I wouldn't put this API in its current form into the platform, I would look at why "Layout Instability" occurs (maybe the top 5 reasons*) and then derive potential scores off of those as they'll be able to be diagnosable for the web developer.

  • I understand that some of these reasons may have nothing to do with the site itself but things between the server serving the content and the rendering engine that the author has no control over (eg: slow network, low end hardware, etc).
@tdresser

This comment has been minimized.

Copy link

tdresser commented Feb 6, 2019

We don't expect the average web developer to touch this API. The common case here will be for RUM analytics vendors to surface this data to users, highlighting issues the developer wasn't aware they had.

You're right that actionability is sometimes tricky here, but the first objective is to make sure developers realize there's a problem. Once developers know there's a problem, we can tackle actionability via:

  • Documentation (e.g., try enabling the unsized media policy and set your font-display values to optional).
  • Local developer tooling letting devs dig in further, with more actionable output.
  • Possibly in the future, better attribution baked into the layout stability API.

Does that seem reasonable? I think step 1 is ensuring folks are aware of the problem and step 2 is making it simple to fix it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.