View Transitions Applied: More performant ::view-transition-group(*) animations

If the dimensions of the ::view-transition-group(*) don’t change between the old and new snapshot, you can optimize its keyframes so that the pseudo-element animates on the compositor.

~

🌟 This post is about View Transitions. If you are not familiar with the basics of it, check out this 30-min talk of mine to get up to speed.

~

# The ::view-transition-group()

With View Transitions, the ::view-transition-group() pseudos are the ones that move around on the screen and whose dimensions get adjusted as part of the View Transition. You can see this in the following visualization when hovering the browser window:

The keyframes to achieve this animation are automatically generated by the browser, as detailed in step 3.9.5 of the setup transition pseudo-elements algorithm.

Set capturedElement’s group keyframes to a new CSSKeyframesRule representing the following CSS, and append it to document’s dynamic view transition style sheet:

@keyframes -ua-view-transition-group-anim-transitionName {
  from {
    transform: transform;
    width: width;
    height: height;
    backdrop-filter: backdropFilter;
  }
}

Note: There are no to keyframes because the relevant ::view-transition-group has styles applied to it. These will be used as the to values.

~

# The Problem

While this all works there one problem with it: the width and height properties are always included in those keyframes, even when the size of the group does not change from its start to end position. Because the width and height properties are present in the keyframes, the resulting animation runs on the main thread, which is typically something you want to avoid.

Having UAs omit the width and height from those keyframes when they don’t change could allow the animation to run on the compositor, but OTOH that would break the predictability of things. If you were to rely on those keyframes to extract size information and the info was not there, your code would break.

TEASER: Some of the engineers on the Blink team have pondered about an optimization in which width and height animations would be allowed to run on the compositor under certain strict conditions. One of those conditions being that the width and height values don’t change between start and end. This optimization has only been exploratory so far, and at the time of writing there is no intention to dig deeper into it because of other priorities.

~

# The Technique

In a previous post I shared how you can get the old and new positions of a transitioned element yourself. This is done by calling a getBoundingClientRect before and after the snapshotting process.

const rectBefore = document.querySelector('.box').getBoundingClientRect();
const t = document.startViewTransition(updateTheDOMSomehow);

await t.ready;
const rectAfter = document.querySelector('.box').getBoundingClientRect();

With this information available, you can calculate the delta between the start and end positions and create your own FLIP keyframes.

Instead of using a transform for the FLIP keyframes (see why below) I will the translate property as that one stacks together with the existing transform on the element. The delta for both the x and y translation is the difference between the start and end position’s left and top respectively.

const flip = [
	`${(rectBefore.left - rectAfter.left)}px ${(rectBefore.top - rectAfter.top)}px`,
	`0px 0px`,
];

const flipKeyframes = {
	translate: flip,
	easing: "ease",
};

Once you have constructed the new keyframes, you can set them on the group by sniffing out the relevant animation and updating the effect’s keyframes.

const boxGroupAnimation = document.getAnimations().find((anim) => {
	return anim.effect.target === document.documentElement &&
	anim.effect.pseudoElement == '::view-transition-group(box)';
});

boxGroupAnimation.effect.setKeyframes(flipKeyframes);

Because the new keyframes don’t include the width and height properties, these animations can now run on the compositor 🙂

🤔 Why not adjust the transform?

In theory you would be able to directly overwrite the transform as follows:

const flip = [
	`translate(${rectBefore.left}px,${rectBefore.top}px)`,
	`translate(${rectAfter.left}px,${rectAfter.top}px)`,
];

const flipKeyframes = {
	transform: flip,
	easing: "ease",
};

In practice this is not usable because the coordinates from rectBefore and rectAfter are viewport-relative, whereas View Transition Pseudos are laid out against the Snapshot Containing Block which – on Mobile Devices – is a different rectangle.

Check out this section from the post on how to get the positions for more details.

Setting a translate on top of the existing transform is not affected by this, because the transform is already SCB-relative and the translate only contains a delta.

UPDATE 2025.03.03 It is possible after all! To adjust the transform directly, you must be able to read the to/from position as SCB-relative values. In https://brm.us/snapshot-containing-block I detail how this can be done.

🤔 Why not directly manipulate the existing keyframes?

You could wonder why the following is not sufficient:

const keyframes = boxGroupAnimation.effect.getKeyframes()

delete keyframes[0].width;
delete keyframes[1].width;
delete keyframes[0].height;
delete keyframes[1].height;

boxGroupAnimation.effect.setKeyframes(keyframes);

Problem here is that Chrome has a bug in which the to keyframe is incorrectly generated and hold incorrect info.

Check out this section from the post on how to get the positions for more details.

~

# Demo

In the following demo the technique detailed above is used.

Here’s the demo that uses translate (standalone version here):

(Instructions: click the document to trigger a change on the page)

~

# Side-by-Side Comparison

In the following demo the default generated animation and my FLIP-hijack version are shown side-by-side so that you can compare how both perform.

Especially on mobile devices the results are remarkable. To exaggerate the effect, the page also adds jank by blocking the main thread for 1 second every 5 seconds.

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.