Most people know that modern web browsers use gpus to render parts of web pages, especially those with animation. For example, CSS animations using the Transform property look smoother than animations using the left and top properties. But if you ask, “How do I get smooth animation from my GPU?” In most cases, you’ll hear advice like “Use transform:translateZ(0) or will-change:transform.
These properties have become like how we use Zoom :1 (if you know what I mean) in Internet Explorer 6 to prepare the GPU for animation or compositing acceleration, as browser vendors like to call it.
But sometimes good, smooth animations that run in a simple demo run very slowly on a real site, creating visual illusions and even crashing browsers. Why does this happen? ** How can we solve it? ** Let’s try to understand.
A disclaimer
Before we dive into GPU acceleration, I want to tell you the most important thing: it’s a giant hack. You won’t find anything in the W3C specification (at least for now) about how composition acceleration works, about how to explicitly place an element on the composition layer, or even about composition acceleration itself. It is simply an optimization of the browser application to perform certain tasks, and each browser vendor implements this optimization in its own way.
Everything you’ll learn in this article is not an official explanation of how compositing acceleration works, but rather the result of experimenting with my own common sense and knowledge of how different browser systems work. There may be some minor mistakes, some of which may change over time – I’ve warned you!
How does synthesis acceleration work
To prepare a GPU animated page, we must understand how it works in the browser, not just follow the advice we get from the web or from this article.
Suppose we have A page that contains elements A and B, each of which has position: Absolute and A different Z-index. The browser will draw it from the CPU, and then send the resulting image to the GPU, which will eventually be displayed on the screen.
<style>
#a, #b {
position: absolute;
}
#a {
left: 30px;
top: 30px;
z-index: 2;
}
#b {
z-index: 1;
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>
Copy the code
We’ve decided to animate the A element with its left attribute and CSS animation:
<style>
#a, #b {
position: absolute;
}
#a {
left: 10px;
top: 10px;
z-index: 2;
animation: move 1s linear;
}
#b {
left: 50px;
top: 50px;
z-index: 1;
}
@keyframes move {
from { left: 30px; }
to { left: 100px; }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>
Copy the code
In this case, for each animation frame, the browser must recalculate the geometry of the elements (i.e., rearrange), and render the image of the new state of the page (i.e., redraw), and then send it again to the GPU to display it on the screen. We all know that redrawing is very performance intensive, but every modern browser wisely redraws only the changed areas of the page, not the entire page. Although the browser can redraw very quickly in most cases, our animations are still not smooth enough.
Rearranging and redrawing the entire page at every step (even incrementally) of the animation sounds really slow, especially for a large and complex layout. It is more efficient to draw two separate images — one for the A element and one for the entire page without the A element — and then simply offset the images relative to each other. In other words, compositing images of cached elements will be accelerated. This is where the GPU shines: its ability to compose quickly with sub-pixel precision adds smoothness to animation.
To optimize composition, the browser must ensure that the ANIMATION’s CSS properties:
- Without affecting the document flow,
- Not dependent on the document flow,
- No redraw will be caused.
You might think that the top and left attributes of absolute and fixed are independent of the element environment, but this is not the case. For example, the left attribute can receive a percentage value depending on the size of the positioned parent; Similarly, EM, VH and other units depend on their environment. In contrast, transform and opacity are the only CSS properties that satisfy the above criteria.
Let’s animate left with a transform instead:
<style>
#a, #b {
position: absolute;
}
#a {
left: 10px;
top: 10px;
z-index: 2;
animation: move 1s linear;
}
#b {
left: 50px;
top: 50px;
z-index: 1;
}
@keyframes move {
from { transform: translateX(0); }
to { transform: translateX(70px); }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>
Copy the code
Here we describe the animation declaratively: where it starts, where it ends, how long it lasts, etc. This tells the browser to update the CSS properties ahead of time. Since the browser doesn’t see any properties that would cause rearrangement or redrawing, it can be optimized by composition: draw two images as composite layers and send them to the GPU.
What are the advantages of this optimization?
- We can get a smooth animation running on a specially optimized unit graphics task with sub-pixel precision, and running very fast.
- Animations are no longer bound to the CPU. Even if you run a very complex JavaScript task, the animation will still run quickly.
Everything seems clear and easy, right? But what problems might we run into? Let’s see how this optimization works.
The GPU is a separate computer, which may surprise you. But it’s true: an important part of every modern device is actually a separate unit, with its own processor and its own memory and data processing model. Like any other application or game, the browser needs to talk to the GPU.
To better understand how this works, think about AJAX. Suppose you want to count visitors to your website from the data they enter into a web form. You can’t just tell the remote server, “Hey, get the data from these input fields and JavaScript variables and save it to the database.” The remote server cannot access memory in the user’s browser. Instead, you must collect the data in the page and turn it into a simple data format that can be easily parsed, such as JSON, and send it to a remote server.
Something similar happens during synthesis. Because the GPU is like a remote server, the browser must first create a payload and then send it to the device. Of course, the GPU is not thousands of kilometers away from the CPU; It’s right there. However, while the 2s required for remote server requests and responses is acceptable in most cases, an additional 3 to 5 milliseconds for a GPU data transfer will result in “Janky” animations.
What is a GPU payload? In most cases, it includes the layer image, along with its additional data, such as layer size, offset, animation parameters, etc. The GPU payload and transfer data here looks something like:
- Draw each composite layer as a separate image
- Prepare layer data (size, offset, opacity, etc.)
- Prepare the shader for the animation (if applicable)
- Send data to GPU
As you can see, every time you add a transform:translateZ (0) or will-change:transform attribute to an element, you start the same process. Redrawing is very expensive and runs even slower. In most cases, the browser cannot incrementally redraw. It must draw the previously covered area with a new composite layer:
Implicit synthesis
Let’s go back to our example of A and B elements. Earlier, we had the A element moving on top of all the other elements on the page. This results in two composition layers: one for the A element and one for the page background layer for the B element. Now, let’s get the B element moving:
We ran into a logic problem. Element B should be on a separate compositing layer, and the final page image of the screen should be composed on the GPU. But element A should appear at the top of element B, and we don’t specify anything about elevating element A itself.
Keep this in mind: special GPU compositing patterns are not part of the CSS specification; It is simply an optimization of the browser’s internal application. We define z-index so that A must appear at the top of B in order. So what does the browser do?
You guessed it! It forces the creation of A new composition layer for element A — and adds another redrawing, of course:
This is called implicit composition: one or more non-composition elements should appear on top of the composite layer promoted in the cascading sequence — that is, drawn as a separate image and then sent to the GPU.
We’ve stumbled upon implicit composition more often than you might think. There are many reasons why browsers promote elements to the composite layer, including:
- 3D transforms:
translate3d
.translateZ
And so on; <video>
.<canvas>
和<iframe>
Elements;- through
Element.animate()
And there aretransform
Animation andopacity
Attribute element; - Through с SS Transitions and animations
transform
Animation andopacity
Attribute element; position: fixed
;will-change
;filter
;
More reasons are described in the Chromium project’s “CompositingReasons. H” file.
It looks as if the main problem with GPU animation is unexpected redraw. But it’s not. The bigger problem is…
Memory consumption
Another gentle reminder that the GPU is a separate computer: not only does it need to send render layer images to the GPU, but it also needs to store them for later reuse in animations.
How much memory does a single composite layer require? Let’s take a simple example. Try to guess how much memory is needed to store a 320 × 240px rectangle filled with a pure #FF0000 color.
A typical Web developer would think, “Well, this is a solid color image. I saved it as PNG and checked its size. It should be less than 1KB. “They are absolutely right: the size of this image as a PNG is 104 bytes.
The problem is that PNG, as well as JPEGs and GIFs, are used to store and transfer image data. To draw such an image onto the screen, the computer must unzip the image format and then represent it as an array of pixels. Therefore, our sample image will require 320×240×3 = 230,400 bytes of computer memory. That is, we multiply the width of the image by its height to get the number of pixels in the image. This is then multiplied by 3, since each pixel is described by three bytes (RGB). If the image contains transparent regions, we multiply this by 4, as additional bytes are required to describe transparency: (RGBa) : 320×240×4 = 307,200 bytes.
Browsers always draw composite layers as RGBa images. There seems to be no effective way to determine if an element contains a transparent region.
Let’s take a possible example: a rotation of 10 photos, each 800 × 600px. To implement a smooth transition between images, such as drag and drop, we add will-change: Transform to each image. This will advance the image to a composite layer so that the transition can begin immediately when the user interacts. Now let’s calculate how much extra memory is needed to display such a rotation: 800×600×4×10≈19MB.
Requires 19 MB of additional memory to render a single control! If you are an application in a single page website of modern Web developers, need a lot of animation control, the parallax effect, high resolution images and other visual enhancements, then an additional 100 to 200 MB per page is just the beginning, you also need to add the implicit synthesis to mix, you will eventually ended up with used devices available memory.
Also, in many cases, this memory will be wasted and show very much the same result:
Fine for desktop client users, but terrible for mobile users. First, most modern devices have high-density screens: multiply the weight of the composite layer image by 4 to 9. Second, mobile devices don’t have as much memory as desktops. For example, a not-so-old iPhone 6 comes with 1 GB of shared memory (that is, memory for RAM and VRAM). Considering that at least a third of this memory is used by the operating system and background processes, and another third is used by the browser and current page (a highly optimized page without the best case of lots of frames), we have at most about 200 to 300 MB of memory left for GPU effects. The iPhone 6 is a fairly expensive high-end device; Cheaper phones have less memory.
You may ask, “Is it possible to store PNG images in a GPU to reduce memory footprint?” Technically, yes, it is possible. The only problem is that the GPU draws the screen pixel by pixel, which means it has to decode the entire PNG image for each pixel over and over again. I suspect animation in this case would be faster than 1 frame per second.
It is worth noting that GPU-specific image compression formats do exist, but they are not even close to PNG or JPEG in terms of compression ratio, and their use is limited by hardware support.
Advantages and disadvantages
Now that we’ve covered some of the basics of GPU animation, let’s summarize its pros and cons.
advantages
- The animation is fast and smooth at 60 frames per second.
- A properly made animation runs in a separate thread and is not blocked by massive JavaScript calculations.
- 3D transformations are “cheap”.
disadvantages
- Add weight painting is required to raise the element level to the composite layer. Sometimes this is very slow (i.e. we get a full layer redraw instead of an increment).
- The drawing layer must be transferred to the GPU. Depending on the number and size of these layers, the transfer can also be very slow. This can cause elements to flicker on low – and mid-market devices.
- Each composite layer consumes additional memory. Memory is a valuable resource on mobile devices. Excessive memory usage can cause your browser to crash.
- If you ignore implicit composition and use slow redraw, in addition to the extra memory usage, the chances of a browser crash are very high.
- We will have visual artifacts, such as text rendering in Safari, where the page content will disappear or be distorted in some cases.
As you can see, GPU animation not only has some very useful and unique advantages, it also has some very nasty problems. The main ones are redrawing and excessive memory usage; Therefore, all of the optimization techniques covered below will address these serious problems.
Browser Settings
Before we start optimizing, we need to understand tools that will help us examine the composite layers on the page, and provide feedback on optimizing efficiency.
SAFARI
Safari’s Web Inspector has a nice “Layers” sidebar that shows all composite Layers and their memory consumption, as well as the reasons for composition. Check out this sidebar:
- In Safari, use
⌘ ⌥ + + I
Open the Web Inspector. If that doesn’t work, go to Preferences > Advanced, open Show Develop Menu in Menu bar, and try again. - When The Web Inspector opens, select the “Elements” option, and then select “Layers” in the right sidebar.
- Now, when you click a DOM node in the main “Elements” window, you will see the layer information for the selected element (if using composition) and for all descendant composite layers.
- Click on a descendant layer to see why it was composited. The browser will tell you why you moved the element onto your compositing layer.
CHROME
Chrome’s Developer Tools has a similar panel, but you must first enable the logo:
- In Chrome, go
Chrome: // flags / # enable-devtools-experiments
“, and enable the “Developer Tools Experiments “flag. - use
⌘ ⌥ + + I
(on the Mac) orCtrl + Shift + I
Open developer Tools (on PC), then click the icon in the upper right corner and select the Settings menu item. - Go to the “Experiments” pane, then enable the “Layers” panel.
- Reopen the developer tools. You should now see the Layers panel.
This panel displays all active composition layers of the current page as a tree. When selecting a layer, you will see information such as its size, memory consumption, number of redraws and composition reasons.
Optimization techniques
Now that we have set up our environment, we can start optimizing the composition layer. We have identified two major issues with compositing: additional redraw, which also allows data to be transferred to the GPU, and additional memory consumption. Therefore, all of the following optimization tips focus on this problem.
Avoid implicit composition
This is the simplest and most obvious technique, and a very important one. Let me remind you that all non-composited DOM elements with explicit compositing reasons (e.g., Position: Fixed, video, CSS animation, etc.) will be forced to be promoted to their own layer just to compositing the final image on the GPU. On mobile devices, this can cause animation to start very slowly.
Let’s take a simple example:
The A element moves when the user interacts with it. If you view this page in the Layers panel, you will not see the additional Layers. But after clicking the “Play” button, you’ll see more layers, which will be deleted as soon as the animation is done. If you look at the process in the Timeline panel, you’ll see that the beginning and end of the animation have been extensively redrawn:
The browser does the following:
- After the page loads, the browser can’t find any reason to compose it, so it chooses the best strategy: draw the entire content of the page on a single background layer.
- By clicking the play button, we explicitly add composition to the element
A
— a person withtransform
The transition animation of the property. But the browser determines the elementA
Lower than the element in the cascading orderB
So it will alsoB
Ascend to one’s own level of synthesis (implicit synthesis). - Ascending to the composition layer always results in redrawing: the browser must create a new texture for the element and remove it from the previous layer.
- The new layers must be transferred to the GPU in order for the user to see the final image composable on the screen. Depending on the number of layers, the size of the texture, and the complexity of the content, redrawing and data transfer can take a significant amount of time to perform. This is why we sometimes see an element flash at the beginning or end of an animation.
- After the animation is done, we go from
A
The reason for removing the composition from the element. The browser saw that it didn’t need to waste resources compositing, so it went back to the best strategy: keep the entire content of the page in a single layer, which meant it had to be drawn on the backgroundA
andB
Layer (another redraw) and send the updated texture to the GPU. This may cause flickering in the steps above.
To get rid of the implicit composition problem and reduce visual illusion, I suggest the following:
- As far as possible in
z-index
Holds the animated object in. Ideally, these elements would bebody
The immediate child of the element. Of course, when the animation elements are nested inDOM
Tree inside and dependent on normal flow, which is not necessarily the case in markup. In this case, you can clone the element and put it inbody
For animation only. - You can give the browser a hint that you are going to synthesize use and have
will-change
Elements of the CSS property. By setting this property on the element, the browser will (but not always) advance it to the composition layer so that the animation can start and stop smoothly. But don’t abuse this property, or your memory consumption will increase dramatically!
Only the animationTRANSFORM
和 OPACITY
attribute
The Transform and opacity properties ensure that they neither affect nor are affected by normal flow or DOM environments (that is, they do not cause rearrangements or redraws, so their animations can be fully unloaded to the GPU). Basically, this means that you can effectively animate movement, scaling, rotation, opacity and affine transformations. Sometimes you may want to emulate other animation types with these properties.
Take a very common example: a background color conversion. The basic method is to add a transition property:
<div id="bg-change"></div> <style> #bg-change { width: 100px; height: 100px; background: red; The transition: 0.4 s background; } #bg-change:hover { background: blue; } </style>Copy the code
In this case, the animation will work entirely on the CPU and be redrawn at each step of the animation. But we can make this animation work on the GPU: Instead of the background-color property of the animation, we add a layer at the top and animate its opacity:
<div id="bg-change"></div> <style> #bg-change { width: 100px; height: 100px; background: red; } #bg-change::before { background: blue; opacity: 0; The transition: opacity 0.4 s; } #bg-change:hover::before { opacity: 1; } </style>Copy the code
This animation will be faster and smoother, but keep in mind that it can lead to implicit compositing and requires extra memory. But in this case, memory consumption can be greatly reduced.
Reduce the size of the composite layer
Take a look at the pictures below. Notice any differences?
The two composite layers are visually identical, but the first weighs 40,000 bytes (39 KB) and the second is only 400 bytes — 100 times smaller. Why is that? Take a look at the code:
<div id="a"></div>
<div id="b"></div>
<style>
#a, #b {
will-change: transform;
}
#a {
width: 100px;
height: 100px;
}
#b {
width: 10px;
height: 10px;
transform: scale(10);
}
</style>
Copy the code
The difference is that # A’s physical size is 100×100px (100×100×4 = 40,000 bytes), while # B is only 10×10px (10 ×10×4 = 400 bytes), scaled to 100×100px using Transform :scale(10). Since # b is a composite layer, the ‘transform’ will appear completely on the GPU during the final image drawing due to the will-change attribute.
The trick is simple: use width and height attributes to reduce the physical size of the composite layer, and then use transform: Scale (…). ‘Extends its texture. Of course, this technique simply reduces the memory consumption of the solid color layer. If you want to make a large photo move, you can reduce it by 5% to 10% and then scale it one level. Users may not see any difference, and you’ll save a few megabytes of valuable memory.
Use CSS TRANSITIONS and animations whenever possible
We already know that the transform and opacity of animations are automatically created as a composition layer through CSS Transitions or animations and work on the GPU. We can also add animations via JavaScript, but we must first add transform:translateZ(0) or will-change:transform, ‘opacity’ to ensure that the element gets its own compositing layer.
JavaScript animation happens when each step is manually calculated in a requestAnimationFrame
callback. Animation via Element.animate()
is a variation of declarative CSS animation.
JavaScript animation occurs every time the callback to the requestAnimationFrame is manually evaluated. Animations implemented through “element.animate ()” are variations of CSS animations with variable declarations.
On the one hand, it’s easy to create a simple and reusable animation using CSS Transition or Animation. On the other hand, when creating complex animations, it is easier to animate using JavaScript than CSS. In addition, JavaScript is the only way to interact with user input.
Which is better? Can we just use a generic JavaScript library to animate everything?
Css-based animation has one very important feature: it works entirely on the GPU. Because you declare how the animation should start and end, the browser can prepare all the required instructions before the animation starts and send them to the GPU. In the case of JavaScript, the browser confirms the state of all current frames. For smooth animation, we had to compute the new frame in the main browser thread and send it to the GPU at least 60 times per second. In addition to computing and sending data much slower than CSS animations, they also depend on the main thread workload:
In the figure above, you can see what happens when the main thread is blocked by intensive JavaScript computation. CSS animations are unaffected, however, because new frames are computed in a separate thread, whereas JavaScript animations must wait for a large number of computations to complete before a new frame is computed.
So, use CSS-BASED animations as much as possible, especially load and progress bars. Because it’s not only fast, it’s not blocked by a lot of JavaScript computation.
An example of optimization
This article is the result of research and experimental development on the Chaos Fighters web page, a mobile game promotion page with lots of animations. When I started development, I only knew how to make GPU-based animation, but I had no idea how it worked. As a result, the first milestone page caused the iPhone 5 — the newest iPhone at the time — to crash within seconds of the page loading. The page now works on even less powerful devices.
I think we should consider some interesting improvements to the site.
At the beginning of the page is the introduction of the game, with something like a red light swirling in the background, which is an infinite loop, non-interactive spinner — perfect for easy CSS animations. The first (misleading) attempt was to save an image of the sun ray, place it on the page as an IMG element, and animate it with infinite CSS:
It doesn’t seem to be a problem. But the sun picture is huge. Mobile users will be unhappy to use.
If you look at the image, it’s basically just a few rays coming from the center of the image. The light is the same, so we can save an image of a single light and reuse it to create the final image. We end up with a single ray image one order of magnitude smaller than the original image.
For this optimization, we have to complicate the tag:.sun will be a container of elements and ray images. Each ray will rotate at a particular Angle.
html, body { overflow: hidden; background: #a02615; padding: 0; margin: 0; } .sun { position: absolute; top: -75px; left: -75px; width: 500px; height: 500px; animation: sun-spin 10s linear infinite; } .sun-ray { width: 250px; height: 40px; background: url(//sergeche.github.io/gpu-article-assets/images/ray.png) no-repeat; /* align rays with sun center */ position: absolute; left: 50%; top: 50%; margin-top: -20px; transform-origin: 0 50%; } $rays: 12; $step: 360 / $rays; @for $i from 1 through $rays { .sun-ray:nth-of-type(#{$i}) { transform: rotate(#{($i - 1) * $step}deg); } } @keyframes sun-spin { from { transform: rotate(0); } to { transform: rotate(360deg); }}Copy the code
The visual result will be the same, but the amount of data transmitted over the network will be lower. However, the dimensions of the composite layers remain the same: 500×500×4≈977KB.
To simplify things, the sun’s rays in our example are fairly small, only 500×500 pixels. Put devices of different sizes (mobile, tablet and desktop) and pixel density on a real site, and the resulting image is about 3000×3000×4 = 36 MB! This is just an animation element on the page.
Look again at the markup for the web page in the Layers panel. We can rotate the entire solar container more easily. Therefore, the container is promoted as a compositing layer and drawn as a single large texture image, which is then sent to the GPU. But because of our simplification, the texture now contains useless data, namely the gaps between the rays.
Furthermore, useless data is much larger in size than useful data! But this is not the best use of memory resources.
The solution to this problem is the same as our network transport optimization: send only useful data (that is, light) to the GPU. We can calculate how much memory we want to save:
- The entire solar container: 500×500×4≈977 KB
- Only twelve rays: 250×40×4×12≈469 KB
Memory consumption will be reduced by three times. To do this, we have to animate each ray separately, rather than as a container for the animation. Therefore, only light images will be sent to the GPU; The gap between them does not take up any resources.
We had to complicate our markup to animate the light independently, where CSS would get in the way. We had already used transform for the initial rotation of the ray, and we had to start the animation from exactly the same Angle and do the 360deg rotation. Basically, we have to create a separate @Keyframes section for each ray, which is a lot of network transmission code.
Write a short JavaScript to handle the initial placement of the light, and allow us to fine-tune the animation, the light count, and so on.
const container = document.querySelector('.sun');
const raysAmount = 12;
const angularVelocity = 0.5;
const rays = createRays(container, raysAmount);
animate();
function animate() {
rays.forEach(ray => {
ray.angle += angularVelocity;
ray.elem.style.transform = `rotate(${ray.angle % 360}deg)`;
});
requestAnimationFrame(animate);
}
function createRays(container, amount) {
const rays = [];
const rotationStep = 360 / amount;
while (amount--) {
const angle = rotationStep * amount;
const elem = document.createElement('div');
elem.className = 'sun-ray';
container.appendChild(elem);
rays.push({elem, angle});
}
return rays;
}
Copy the code
The new animation looks the same as the previous one, but actually uses twice as much memory as the previous one.
Not only that, but in terms of layout composition, the animated sun is not the main element, but a background element. The light doesn’t have any clear contrast elements. This meant that we could send a lower-resolution ray texture to the GPU and then upgrade it, which allowed us to reduce memory consumption a bit.
Let’s try reducing the size of the texture by 10%. The physical dimensions of the light will be 250×0.9×40×0.9 = 225×36 pixels. To make the light look like 250×20, we had to upgrade it 250 ÷ 225 ≈ 1.111.
We’ll add a line to our code — background-size: cover for.sun-ray — so that the background image is automatically resized to the element, and we’ll add Transform: Scale (1.111) to the ray animation.
const container = document.querySelector('.sun');
const raysAmount = 12;
const angularVelocity = 0.5;
const downscale = 0.1;
const rays = createRays(container, raysAmount, downscale);
animate();
function animate() {
rays.forEach(ray => {
ray.angle += angularVelocity;
ray.elem.style.transform = `rotate(${ray.angle % 360}deg) scale(${ray.scale})`;
});
requestAnimationFrame(animate);
}
function createRays(container, amount, downscale) {
const rays = [];
const rotationStep = 360 / amount;
while (amount--) {
const angle = rotationStep * amount;
const elem = document.createElement('div');
elem.className = 'sun-ray';
container.appendChild(elem);
let scale = 1;
if (downscale) {
const origWidth = elem.offsetWidth, origHeight = elem.offsetHeight;
const width = origWidth * (1 - downscale);
const height = origHeight * (1 - downscale);
elem.style.width = width + 'px';
elem.style.height = height + 'px';
scale = origWidth / width;
}
rays.push({elem, angle, scale});
}
return rays;
}
Copy the code
Notice that we only change the size of the element; The size of the PNG image remains the same. Rectangles created by DOM elements will render as GPU textures instead of PNG images.
The new composition size of solar rays on the GPU is now 225×36×4×12≈380 KB (it was 469 KB). We reduced memory consumption by 19% and got very flexible code that we could scale down to get the best quality-memory ratio. So, by increasing the complexity of the animation, which looks so simple, we have reduced memory consumption by 977 ÷ 380≈2.5 times!
I think you’ve noticed that this solution has a major flaw: animations now work on the CPU and are blocked by a lot of JavaScript computation. If you want to get more familiar with how to optimize GPU animation, I have a little homework assignment. In this demo, Codepen of the Sun Rays allows the sun Ray animation to work entirely on the GPU, while ensuring memory efficiency and elasticity like in the original example. Post your examples in the comments for feedback.
course
- Optimizing the Chaos Fighters page made me completely rethink the development process of modern web pages. Here are my main principles:
- Always discuss all animations and effects on the site with clients and designers. It greatly affects the markup of the page for better compositing.
- Pay attention to the number and size of composite layers from the start — especially layers created through implicit composition. The Layers panel in the browser development tool is your best friend.
- Modern browsers use compositing not only extensively for animation, but also to optimize the rendering of page elements. For example,
position:fixed
.iframe
andvideo
The element uses composition. - The size of the composite layer may be more important than the number of layers. In some cases, the browser will try to reduce the number of composite layers (see the “GPU-accelerated Composite in Chrome” Layer Compression section); This prevents so-called “layer explosions” and reduces memory consumption, especially when layers have large intersections. Sometimes, however, this optimization can have a negative effect, such as when very large textures consume more memory than several small layers. To get around this optimization, I add a small, unique to each element
translateZ()
Value, such asTranslateZ (0.0001 px)
.TranslateZ (0.0002 px)
And so on. The browser determines which planes the elements are on in the 3D space and therefore skips optimizations. - You can’t just add to any random element
transform:translateZ(0)
orwill-change:transform
To virtually improve animation performance or get rid of visual illusion. GPU compositing has many drawbacks and trade-offs. When not in use, compositing can degrade overall performance and even cause the browser to crash.
Let me remind you: There is no official specification for GPU compositing, and each browser solves different problems. Parts of this article may be out of date in a few months. For example, Google Chrome developers are exploring ways to reduce the overhead of cpu-to-GPU data transfers, including using special shared memory with zero copy overhead. And Safari has been able to delegate the drawing of simple elements (such as empty DOM elements with backbackground color) to the GPU instead of creating its images on the CPU.
Anyway, I hope this article has helped you better understand how browsers use gpus for rendering so you can create impressive websites that run quickly on all devices.
This article is translated from GPU Animation: Doing It Right by @Sergey Chikuyonok. The whole translation is based on our own understanding and thoughts. If the translation is not good or there is something wrong, we would like to ask our friends for advice. If you want to reprint this translation, it is necessary to indicate the English reference: www.smashingmagazine.com/2016/12/gpu… .