This article broadly covers the following areas:
In Computer Science, field performance can mean a lot of things. But here, I will be covering web performance, particularly user-centric performance — and this is exactly what the RAIL model is based on. This model is based on 4 different types of key actions that users perform in any app — Response, Animation, Idle, and Load (RAIL). Defining and achieving goals for each of these will help improve user experience.
Response to user interactions — like a tap, click, and toggle should be completed within 100ms so users feel like interactions are instantaneous.
Users are good at tracking motion, and they dislike it when animations aren’t smooth. Animations appear to be smooth as long as 60 new frames are rendered every second (more about this later). So, the time allotted for each frame to render is 16ms (1000/60) per frame, which includes the time it takes for the browser to paint a new frame on the screen. Since browsers need about 6ms to render each frame, the developer is left with approximately 10ms to produce a frame.
If the frame takes more than 10ms to render, it will be dropped, and the user will experience juddering/janking.
Maximize idle time to increase the odds that the page responds to user input within 50ms. We don’t want to block the main thread from responding to user interaction. To use idle time wisely, the work is grouped into blocks of about 50 milliseconds. Why? Should a user begin interacting, we’ll want to respond to them within the 100-millisecond response window and not be stuck in the middle of a 2-second template rendering.
Deliver content and become interactive within 5 seconds for low or mid-range mobile phones with slow 3G connections. Adding a performance budget, tracking competitors, and various other factors also come into play. Reaching this goal requires prioritizing the critical rendering path and, often, deferring subsequent non-essential loads to periods of idle time (or lazy-loading them on demand).
To sum it up, here are the goals to keep in mind for each of the 4 factors of the RAIL model:
Response | Animation | Idle | Page Load |
---|---|---|---|
Tap to paint in less than 100ms. | Each frame completes in less than 16ms. | Use idle time To proactively schedule work. | Satisfy the "response" goals during full load. |
Drag to paint in less than 16ms. | Complete that work in 50ms chunks. | Get first meaningful paint in 1,000ms. |
Now, let’s understand more about a frame and the rendering process it goes through.
When it comes to performance, we might encounter various types of problems. Let's consider a particular type of issue. For a website having a parallax effect that will go through constant re-rendering and re-painting, you might notice some juddering. Painting takes a lot of CPU time, causing frames to be dropped. This is especially true of devices that are low on CPU power. And painting takes a lot of CPU time, causing frames to be dropped.
if you see the above gif, you will notice juddering and continuous re-painting (green flashes highlights re-painting) happening on the continuous scroll, which could be one of the reasons for the frames getting dropped. But before we jump to the solution, here is an overview of Pixel Pipeline (frame journey) to understand more about the problem.
Previously, we discussed why frames need to be generated in less than 10ms to keep animations smooth. Now, let’s look at the pixel pipeline — or render pipeline — to understand the frame journey and learn how to avoid juddering or janking issues.
The first thing that happens in each cycle is that any pending javascript is run. Typically, anything that triggers visual change is part of this step. Here are some pointers to keep in mind to optimize JS Execution:
This is the process of figuring out which CSS rules apply to which elements based on matching selectors.
Once the browser knows which rules apply to an element, it can calculate how much space it takes up and where it is on the screen. Properties like position, width, margin and display all affect how an element is laid out on the page. Many of these, such as increasing an element’s height, also affect the layout of elements further down the page as it pushes on them. Consequently, these properties tend to be costly to update since you almost always end up updating other elements as well. For animation, they should really be avoided as much as possible. (Below we will see in action)
It is a process of filling in pixels. It involves drawing out text, colors, images, borders, and shadows. The painting actually involves 2 tasks:
The rendering pipeline’s final step is to combine the different layers into a single view for the screen, possibly with some manipulation of the layers first.
The more a frame can escape the pipeline’s steps, the more performant it will be since it will take less time to render and can avoid potential janking.
Let me show how we can find areas getting re-painted using dev tools.
After you open devtools, press Cmd + Shift + P, and type show rendering
. You will get many options to measure, click on Paint flashing, and interact with the app.
The green flashing rectangles in the above GIF show the area getting re-painted as I continuously scroll.
We can solve this by detaching the hover event when the user is scrolling and attaching it back when the user stops. Here’s what scrolling through the same page looks like post-optimization:
As you can see, the green flashing rectangles no longer appear when I scroll. They appear when I stop scrolling, keeping the desired output intact while also improving rendering performance.
Now that we know how to improve re-painted areas, let’s look at the layout (rendering) part of the pixel timeline.
Above is a screenshot of the performance tab present in dev tools post profiling. The first-row shows FPS, CPU, and NET. The purple color represents rendering, and this screenshot show CPU is occupied with continuous re-rendering. Also, the red bar you see above in line with FPS — this indicates frames getting dropped, which in turn means that the animation is not smooth.
Re-rendering usually happens when the layout of the frame changes — when we change properties like position, height, margin, or display — thus affecting how an element is laid out on the page. So, in code for hover on the image, I am using the following CSS code:
#demo p img {
position: relative;
cursor: pointer;
height: 100%;
transition: all 0.3s;
}
#demo p.hover img:hover {
box-shadow: 0 0 12px 13px #ccc;
top: -12px;
height: 105%;
left: 10px;
}
On hover here, we are changing all the properties, which will affect the layout of the element and its neighboring elements.
One way to solve this is to use Compositor-Only Properties, which escalates the frame into a new layer and runs separately in the GPU, keeping the main thread idle, which will optimize frame delivery. So, I made the following change:
#demo p img {
position: relative;
cursor: pointer;
height: 100%;
transition: all 0.3s;
}
#demo p.hover img:hover {
transform: translateY(-12px);
transform: translateX(10px);
transform: scale(1.05);
}
Profiling again after making the change now gives me the following output:
If you compare the two screenshots, you’ll see that the time spent by the CPU on rendering has reduced drastically. The app is also now jank free as very few frames are being dropped. The end result? A far smoother experience for users ✨
If this helped you learn something new today, show some love! 👏 Thank You!
PS. If you’re a performance geek, here are my references. Feel free to dive in.