 I'm Adam Margo. I'm Jason Miller. We're here to talk to you about smooth moves. When we were thinking of what to talk about at CDS this year, we wanted to demonstrate some techniques for optimizing performance of existing applications. So we were trying to think of what's like an app that Google doesn't have, something not in our portfolio. It took us a while to come up with something, but I think we kind of hit the nail on the head. A chat app. They don't have a chat app. No. It's French. This is an app for cats. Sorry, I knew this slide would be really confusing. So it's a chat app. We've been working on an app that actually facilitates communication between cats. No cats are working on this uprising against humans of sorts. And we figured that's sort of like an opportunity for an app that helps out with this, with the planning and whatnot. Yeah. So this is what our app looks like. Design is sort of a mix of typical material stuff and takes some inspiration from Android messages, among others. And we wanted the UI to be realistically complex and well-designed, so that would be challenging enough for the browser. And we could measure our optimizations. And obviously, the text that's in there, that's cat ipsum. I don't speak feline. I don't know what's going on. I do, but I refuse to make a demo. So next, we turned on the performance monitor, right, because it looked good on my machine. Let's measure it. It seems to look like everything's decent enough. Yeah. So we saw a bit of jank. And some of the early user feedback we got was that the app felt slow. We applied a bunch of the techniques that Katie and Hussein talked about in their talk earlier today. But it wasn't cutting it. And there was something else wrong. So we decided to load the video up on a Nexus 5. It's really useful to have a device like this sitting around as you develop for the web. As developers, we often have the privilege of fast devices on fast connections. And that can kind of trick us into thinking that we're meeting our performance goals when we're actually not out in the real world. So watching the video, you can see it's far from smooth. Many types of jank are present. So smooth. It's a word we hear a lot when we're talking about user interface. And it's true. And it can be difficult to describe, since it's sort of an invisible metric. If your app is smooth, no one really notices anything is wrong. So then what does that really mean that smooth is? We thought smooth could destructure into these attributes really well. So let's break them down. Cool. Smoothness is something we believe that happens when the user feels connected to the app that they're using, like they're directly controlling it, a sort of physical connection to the interface. When the application doesn't respond quickly to input, it breaks this connection. And we feel distanced from the task at hand. It's best when it feels like magic paper. Right. So when an application animates irregularly, it actually distracts us by breaking something called the illusion of motion. You can see an illustration of what I'm talking about up here on the right. The eye responds to light changes at 10 times per second. So if we show the eye a sequence of 10 images per second, we can actually still see each individual frame. Yeah. So the brain doesn't necessarily interpret that as motion. If we increase this to 20 frames per second, somewhere in and around the red orb there, we start to interpret this as motion because we've exceeded two times the number of light changes we can detect. And this effect increases as we approach 60 frames per second where we start to see some diminishing returns. Basically, smoothness relates to the human perception of performance. It's a measure of the question, is this application keeping up with me? Or does this feel like it's helping me get something done or is it getting in my way? The way the user feels when interacting with software is really important. Because we've all been in situations like this. He was obviously trying to send a tweet, and it just wasn't working out. You probably should have used some newer hardware. Well, he's breaking it now. So this is backed up by statistics, too. In a 2017 study, 46% of people who had an interruptive mobile experience said they wouldn't purchase from that brand again in the future. And in that same study, they found that 79% of people said they're more likely to revisit or share a mobile site that was easy to use. And another metric is a good one. 100% of atoms say they doubt a thing's reliability if it hiccups while performing its task. Maybe you agree. Right. So anytime we talk about the perception of performance, it's useful to frame things using the RAIL model. RAIL provides a set of goals that cover the four ways we all perceive performance and speed when using software. Those are response, which is reacting to events in 100 milliseconds, animation, which is producing frames in 16 milliseconds, idle, which is maximizing idle time, and load, which is loading in under one second. And these are based off of a few years, a few decades, actually, worth of research. So they're very unlikely to change. And you know what the slide looks like to me is click, run, swipe, and load. Yeah, we could have used a different acronym there, I guess. Yeah, we'll get there. Cool. Yeah, so there's times that we have to hit for all of these. And we believe responding to user input quickly is the first and arguably most important thing to do in your app for it to feel or appear smooth and snappy. Right. Even sticking to RAIL's goals, though, idle work can potentially delay input by up to 50 milliseconds, which we're going to hear about in a second. We recommend responding to input in 50 milliseconds in order to have a combined time never exceed 100. That's why you'll see the difference between the goal and the guideline. Animation is a similar story, so there's more than just our code that has to run in order to get a frame on the screen. To account for a reasonable amount of browser rendering overhead, we recommend shooting for 10 milliseconds of your JavaScript time to produce that frame, leaving six left over for the browser. Browsers are so cool. We're talking about six milliseconds, 10 milliseconds. Who could do anything in the amount of time the browser is doing crazy stuff so rad? So in addition to deferring as much work as possible, it's also good to defer your work in 50 millisecond chunks. This is where that response metric gets its differentiation from. So if you have 50 millisecond blocking idle tasks, there's a chance that any one of those could cause input to be queued for 50 milliseconds, which means if you need to respond in 100, you only have 50 milliseconds left. So that's where that metric comes from. And the load goal is based on research showing that we generally lose focus after about one second. We start to feel a bit disconnected from the activity we're doing. And then at 10 seconds, we become frustrated. We go grab a sledgehammer and probably light something on fire. That's how that picture happened. So Rails' guideline for load is kind of right in the middle of those two numbers. We set it at five seconds. And this is a good metric for something like a page load, where you're waiting for a decent amount of new content to come in, even a whole screen's worth of content. For little interactions, though, like the sending button in our chat app, it's actually best to shoot for that one-second goal. There's only a small amount of content being changed on screen, so it's not as perceivable to the user. Yep. And with Braille, we have a nice mental model for figuring out why the app feels slow. And it helps us set a useful challenge. And you'll notice on each slide as we continue here, there'll be a little badge in the top right. And it'll have an R, an A, an I, and an L. And it'll pertain to the real model. Right, so we're pretty confident that we can meet real guidelines on a $1,500 laptop, or maybe some other very popular laptop. Yeah, that's a kush laptop there. It kind of looks like it's on Jon Snow's bed or something like that. Yeah, I've never seen Game of Thrones. I assume that's the reference to Game of Thrones. So we can do it on the laptop. But can we meet our guidelines on a sub $100 phone? Doi, the web is awesome. Yes. So we want our apps to be smooth everywhere. And what we need to do to ensure that is to optimize for the lowest common denominator. We need to be 60 frames per second on low end hardware. So before we embark on a challenge like this, though, we need to be able to measure whether we're meeting those guidelines or not. Right. So all the tools that we have available to measure these things live in Chrome DevTools. DevTools. We have seen a bunch of them earlier today. In Paul and Elizabeth's talk, they talked specifically about DevTools. We're actually going to show a couple of features in DevTools that are maybe a little bit less commonly used, some of them potentially even lost to the sands of time. So let's get into them. The first tool is down on the drawer, which you can pull up using the escape key. It's the rendering panel. It lets you visualize important things that we're going to get into today, like painting layer borders and live FPS meter. Right. The second useful tool is up in the three dot menu in the upper right. That's called layers. It shows a real time view of the layers that make up your page. We're going to deep dive into this in a little bit. I love that view. But it's useful to remember that DevTools has you covered here. The next tool is back down on the drawer again. It's the graphs we showed earlier called the performance monitor. And here you can monitor various aspects of your app's performance, chart it over time, plus it looks so good on a dark theme. Just love that. It's pretty useful to find hiccups. Right. Finally, there's Lighthouse. And we've seen a couple of times today this used for tracking load performance metrics. But you can actually find it's slightly useful to find runtime metrics in Lighthouse, particularly for input response delays. So Lighthouse doesn't track for very long after page load, but it does for a little bit. And this might be exactly the kind of heads up you need in order to figure out that there are some optimizations left for you to do on your website. Cool. All right. It's business time. We're equipped with some tools and strategies and how to investigate performance. Let's dive in. So we came up with three types of optimizations that we hope will help address common performance issues. We have efficient animations, reading, and then writing. And then sort of a grab bag of things that we're calling lazy wins. So that's sort of a mix. So we'll start off with efficient animations. Animating things effectively in the browser really requires a somewhat deep understanding of how browsers render. Let's say we want to build the chat UI that we showed in the intro. Chat UI. Right, chat, sorry. We want to write some HTML, give it a bunch of fancy styling, and then arbitrary magic happens and you have pixels on a screen. That level of understanding is not enough for us to be able to optimize here. So let's dig into it. We start off with some HTML. This all gets parsed and turned into a tree structure that we call the DOM, we're familiar with the DOM. And this sort of preps us to the point where we're ready to render, which is a four-step process. The first step is style calculation. So the browser needs to compute all the styles for the elements that you see, which is resolving things to their final values, resolving CSS custom properties, inherited values, et cetera. And it does this by going through all the elements and figuring out which CSS properties apply to them. With those values calculated, we can figure out the positions of where all those elements are on the screen. So you see those are represented as boxes up here on the right. The cost of this calculation step varies quite a bit. Some types of layouts are multi-pass, which means that they need to be laid out multiple times before they're in their final resting state. And other types, like position absolute, are just static. So they only need to be one pass. So that computed layout has enough information to break things up into pieces that we call paint layers. Here, in the painting process, we walk through all the paint layers, and we convert the layout information that we had into draw commands. And these sort of look like the 2D Canvas API if you've used that. So Chrome takes these, and it sends them over to its graphics layer, which is called Skiah. And those get rasterized and sent back as image representations. Wait a sec. Chrome, there's Skiahs? I thought they were snowboarders. Yeah, no, all Skiahs. OK. So with the paint layers rasterized, the last step is to composite them. So this step takes all the rasterized, essentially, images at this point, and lays them out on top of each other in order to make things look like the final page. So what you get is the results on the right, which is what we wanted. This whole process applies anytime you have page updates that are triggered by JavaScript, manipulating the DOM. So let's say we want to update the left property of a div, left position of a div. The browser is going to have to recalculate styles. It's going to have to redo the layout because they have changed. Paint all the layout, or the paint layers that changed, and then composite them together to form the resulting page. Yeah, a lot of work. A lot of work. And then if we set the transform property for some contrast, transform, opacity, and filter are all what's called compositable properties. So modifying these doesn't actually trigger layout and can often bypass paint. So you go straight from style recalculation to compositing, maybe to paint. Yeah, so a lot less work. These things, they get really important when you're animating. On every frame, we are now potentially computing layout if you've set something like width or left. And this is a lot of work for the browser to do. This is going to be a low frame rate. Animating something like color, layout isn't required because you're not changing the bounds of the position of the element. So on each frame, you're recalculating styles, painting, and compositing. You skip the layout step. Yeah, it's a little better. This used to be a performance concern, but in modern graphics pipelines, it's actually pretty fast. Finally, animating compositable properties, like transform opacity, all that work is handled by the compositor. So this is really good for things like responding to pan gestures because it's important to keep that pipeline as short as possible. The one thing to keep in mind, though, is that compositing is not free. If you're on resource constrained devices, you really need to watch your composited layer count. If it gets too high, you can hit memory. Yeah, you pretty much have jank permanently. So we had a send button in our application. And depending on whether it was enabled or disabled, whether there was text in the text field beside it, it would have a large or a small box shadow and change its color. So originally, we just did a CSS transition on the box shadow. Box shadows are paint-based animations because they physically cannot affect layout. And that meant that on every frame that we had to do was send a bunch of new commands over to Skiya, like we saw in those demos. So here's an example of what that might look like in a three-frame animation. And you see it's pretty much repeating the same process each time with new variables. We tried switching this to a composited animation. And the way we did that was we duplicated the button using a pseudo-element. And then the foreground was the element, and the background was a pseudo-element with the same shape, but a box shadow applied to it. And then all we have to do is change the opacity of the box shadow pseudo-element behind the button. And we can get any of the frames of the animation we want between 0 and 100 using just composited animations. So any frame can be constructed from just those two paints. We do not need to repaint. You can see this in action, too. So looking at DevTools. I love this GIF. It's so cool. Yeah, we've got a paint-based animation on the top. That's just box shadow. The center one is animating the opacity of a shadow element. And then the bottom one is sort of the penultimate. This is animating the transforms scale of the shadow. Hey, where's the middle one paint at the end? The end beginning. Yeah, so paints at the beginning, paints at the end. That's when it's being promoted and then demoted to a composited layer. That was so quick. He was barely promoted. Right. That's a bummer. So we did this experiment. The profiler told us that this was a good idea. Obviously, there's a lot less main thread work going on in the second two animations. These are in order. And so this might be something we would want to do, except if we're targeting really low-end mobile devices where they might not have the best GPUs. The reason why this is good is because GPUs are really good at transforming composited layers. It's important to remember that this is not a silver bullet, though. You always want to make sure that you profile and that your performance improvements on desktop don't become problems on mobile. Yeah, measure. And please, so we don't have context for this particular slide, but please don't animate max height. This is something that feels really easy and really good when you do it. But it can be really, really brutal down the line for performance. I honestly don't think that's what people even wanted to do. It's like you didn't want to take something that was squishy and all warped and make it unwarped. I think you really wanted to reveal the element. And I think when we were doing our performance optimizations, we just switched our max height animation to be slide in. Slide in. We just used transform. Better anyway. No. OK, so we talked about efficient animations, but there's another whole aspect of rendering that's really important if you want to hit that 60 frames per second target. And that's this idea of reading and then writing. To understand this, we need to take a trip down to the rendering assembly line. Yay. So ideally, when we look at an app in the performance tab of DevTools, we see little chunks of this sequence. And these are the frames. And in an ideal situation, there's a lot of white space in between these, which means that you don't have a lot of idle time work. Your app is sort of main thread, jank freeze. This looks like Jake Archibald's socks. I think those were the color profile we went with. Yeah, so we peaked at the anatomy of a frame earlier. We have style recalculation, layout, paint, compositing. We didn't really dig into the script portion, that initial portion that leads off this whole thing. And as it turns out, it's really important. Not all scripts are created equal. So our interaction with the DOM affects every other step in the rendering process. And we can split out the two types of scripting into two groups. We have read and then write. On the left, you can see some examples of properties that are DOM layout reads. Some of these are pretty obvious, offset top. Obviously, if you want to know where the element is, you need to lay it out. Some of these are not so obvious, though. Intertext inserts, it's a string. It's kind of like text content. But there's new lines in it based on layout. If you have paragraphs or elements with display block, the string has new lines in it. And it's impossible to return that string without calculating layout first. If you're using intertext in your app and you're not using it specifically for that feature, I would strongly recommend going with text content. On the right is maybe a little bit more obvious. If you change CSS layout, add elements, mutate the DOM tree structure, you're going to trigger layout. I wish it was as simple as like getters on the left and setters on the right. So let's say we have a timeline like before. But this time there's two bits of script that are going to run. The first sets the width of an element to 10 pixels. The second script, maybe it's a different script, asks how much space is left beside foo? How much space is left beside that div? And in order to do this, it's going to use the offset width property. This is where things fall apart. We hadn't had a chance to calculate style and layout. So we have to do that synchronously now, which is going to be forced. So we have to block the main thread, do all that work, and then come back to the script that asked for that width. And the worst part is that script actually did this to conditionally then set the width of foo, which invalidates the layout and the style calculations we did, which means that they have to happen again. I smell code smell. Right. Yeah, so this is a problem and this is really common. You might be tempted to use request animation frame to fix this. That's actually often a good strategy. Let's see how that would work. So request animation frame callbacks fire just before the browser does all of its rendering work. And let's say we might read some values synchronously in the script block on the left and then in queue an animation frame callback where we do our DOM rights. So you're doing read and then write using this raft callback. For really well-behaved code, this is actually an awesome way to give the browser more control over and insight into your rendering code. Yeah, it's like polite. You're like, hey, whenever you're ready. Right. So if you pop up in the profiler on an arbitrary website, you might find this technique in play where it's not actually panning out. And the reason for that is deferring reads using request animation frame doesn't really fix this problem. It's still going to trigger forced synchronous layout inside of the raft callback, which can sometimes be worse. That's tricky. Yeah, so we're gonna end up in the same situation we had before. So reading properties that require layout information before modifying layout is an important thing to keep in mind when interacting with the DOM. This brings us to our variety portion of the show, which we have called lazy wins. All right, and so earlier we were talking about smooth and as described as connected, silky and asynchronous. And I feel like this crowd is savvy about the topic of sync versus async and how to be polite to threads or thread. But it's not often enough that we are considerate of our users thread. They're often pressed for time and resources as well. So we should do our best to keep their thread free of locking. Our first version had synchronous interactions, just like the animation here. It's kind of painful to watch, right? You're like, hey, what? I hope it's done. Okay. So here's a demo of us sending. It's called rough moves. You'll like that. It's notice how this one, we have a new message or thought can't be started until the previous one is finished. And it's not too bad when you send one message, but when you're sending many, you're gonna be super disruptive. The user interaction implementation was easier for the engineers because they could handle the flow being synchronous about it. It's sort of like less work for me, but we redid it. And so here's the same thought process being typed out. And these messages are being sent using asynchronous, almost sort of known as optimistic interactions. And the user is never blocked. They're free to express themselves at the rate in which they desire and are capable. And this can increase the perceived performance of your application because it doesn't appear to stall or think too hard about certain tasks. Measuring smoothness as a UX metric can be a difficult thing to quantify or qualify. But luckily the experts at Google have devised a fantastic framework for evaluating the quality of UX and therefore its perceived performance. It is called heart. And I wanted to throw this in there because it's a sort of mix of lazy wins because it can help you measure the impact of your UI interaction paradigms. Cool. So one quick way to improve your application's performance is to just find a browser primitive that is more efficient than something manual you're doing today. A great example of this would be position sticky. The manual implementation of position sticky requires querying for layout information while you're scrolling in the scroll handler. This is really expensive. This triggers all the bad performance problems we saw before. Another one would be to leverage native scrolling by swapping out your custom scroll implementation, scroll smoothing implementation for scroll interview with behavior smooth. This is relatively new. This feature is awesome. It made building our chat UI really easy. The only thing that we had to do, we were firing it a lot. So we actually debounced it using request idle callback. So we only scroll interview once the main threads settled down and messages have stopped being sent. That was a really slick one too because on an old device, we had a test where we hammered messages in there. We sent like 30 of them over at the span of like three seconds. And an older device was able to not be overwhelmed because we were nice about asking when to smooth scroll. So the messages would pour in it and then smooth down to the bottom. It'll wait. It'll wait. And on a nice machine, they just poured right in because it was powerful enough. It was easy. Okay, so just like scrolling, panning is a gesture where we expect to feel like we're physically pushing content around on the screen. We want to have that connection between our thumb and the pixels. In a well-built custom panning implementation, touch input received by the browser is sent to the page and then the JavaScript input handling code responds by updating probably the transform property on an element. And then that is rendered by sending that back to the compositor, which results in pixels on the screen. So this actually works pretty well, but we can do better. In browser scrolling, optimal browser scrolling, we don't have to wait for JavaScript event handlers to complete. We could just directly translate that layer on the compositor. Events will eventually get sent over to the page, but in an ideal setup where passive event listeners are being used, they can be firing for get. And this is gonna get you your shortest time to pixels. So here's an example of that manual kind of panning, the first kind. It's a simple carousel. These are my pets, by the way, my wife required that I said that. So it's using pointer events and it sets a transform property on the layer. There's some problems with this though. So we're getting composited animation, which is good, but the implementation is non-trivial, especially if you wanna integrate really well with the browser's own page scrolling. If you're panning and the person's thumb starts to move more up than left or right, you really need to cancel that animation. Get that uncanny, scrolly valley. Yeah. So another option here would be to use something called scroll snaps. So this is really elegant. Basically, instead of all the manual implementation, you just put two CSS properties on that carousel. The first is scroll snap type. We just said we wanna do it in the x-axis and always snap. And the second is scroll snap a line, which just tells it to snap to the beginning of each image. So cool. And what this looks like is this. So this requires a lot less code to implement. It's still composited, but this one doesn't rely on the main thread at all and it feels natural on each platform because the easing rather than being manually implemented is just the platform's native scroll. Yeah, you get to bounce at the end for free. You get to throw it, I mean, all nine, right? So to sum things up, make sure you're keeping the browser's rendering pipeline in mind when you're animating and always try to avoid animating layout. Sync up your DOM access so you read properties that require layout information before mutating the DOM and causing layout. All right, and consider using optimistic interactions because they can sometimes be better than optimizing rendering, especially if you're just getting started. Right. And finally, take advantage of places where you can offload the most performance critical work and rendering to the browser. If you can leverage something like scroll snapping or position sticky to implement your designs, that might be the biggest performance win of all. It's a lazy win. Right. Cool, so thanks, everybody. Thank you.