Visualising Front-End Performance Bottlenecks

Bookmark

There are many ways to measure web performance, but the most important thing is to measure what actually matters to users. This talk is about how to measure, analyze and fix slow running JavaScript code using browser APIs.



Transcription


♪ Hello. Before we get started, I would just like to give a shout out to the organisers and the fellow speakers at React Summit. It's been a really great conference so far and you're all doing a great job. Today I'll be talking about web performance. My name is Rich. I'm known as Richie McCall on GitHub and Twitter. I'm a front end engineer, and I work at a company called DAZN in London. I'm from Glasgow, Scotland, originally. You can probably tell from the accent. And before I start talking about web performance, I'll just give a quick introduction into what we do at DAZN. So DAZN is a live and on-demand sports streaming service. And we're live in nine markets around the world, and we're providing millions of customers with access to the sports that they would like to watch. The team I'm working on is responsible for living room devices. And what living room devices is can be broken down into three categories. So we have smart TVs, which is like Samsung, Toshiba, Panasonic TVs. We've got games consoles, which is PS4, PS5, Xbox, and also set-top boxes, which is Comcast, Fire TV and SkyQ. And so to put performance in the context of what we do at DAZN, the kind of problems that we face are to do with low memory and CPU targets, resource competition on the main thread. For example, you have playback running in the background and the user's trying to navigate content. At the same time, maintaining smooth 60 frames per second interactions when the customers are navigating content. And the data doesn't lie, right? Fast websites equals a better user experience. This is what customers prefer, and it's something that we should be striving towards. So this data from the Cloudflare getting faster white paper demonstrates some kind of useful quotes. Conversions go down 7% due to just one additional second of load time. And 39% of users stop engaging with a website if images take too long to load. And another great quote from Craig Maud's essay on fast software is that, to me, speedy software is the difference between an application smoothly integrated into your life and one called upon with great reluctance. And as users of software, we can definitely relate to this at one point or another. So today what we'll be covering is understanding the method of measure, analyzing, and fixing. We'll briefly touch on the user time and API that the browser provides. We'll have a live demo measuring and analyzing slower in JavaScript code. I'll introduce the rendering technique of virtualization. I'll speak briefly about the performance problems we face at the zone. And finally, I'll fix the performance bottleneck that we have in the demo. The measure, analyze, and fix cycle is a methodology that I tend to use when doing performance audits. So before we can analyze and fix a problem, we first have to measure. Measure will give us the baseline that we need to analyze the problem, really. Once we analyze the problem, we can then propose a fix. Once we fix, we then measure again. And this cycle repeats until we've got a new number that we're happy with when we measure. I've got a demo, small front end application. It's available on this GitHub URL. The setup instructions are on the readme. So if you fancy following along, then please feel free. So I'll switch over to the demo here that I've got running. And this application is just a small front end application that uses the SpaceX API to render a list of launches from new to old. And the interaction that we'll be profiling is changing the order of launches. So if I click this button, I'm now viewing the oldest launches from SpaceX. So to profile this thing, what we'll do is go into developer tools in Chrome and we'll use the performance panel. And we'll come to the CPU option here and click six times slowdown. We'll then come over here and press record. And what we'll do is we'll just interact with this feature a few times just to get some data. And stop the recording. OK, great. So what we can see here is there's quite a lot going on. What we're really concerned about is this section here. So this section is the main thread. And for those who aren't aware of what happens on the main thread, I'll quickly jump over to a different slide and we can explain the theory behind it. So this image here is from Paul Lewis's blog post called the anatomy of a frame. And essentially the browser performs a different set of tasks at any given time to get stuff on the screen. So when we're profiling and we see any yellow, we know that JavaScript has been executed. Anytime we see purple, we know that style or layout has been calculated. And anytime we see green, we know that there's some paint or compositing to the screen happening. So if I switch back over to that demo, now that we understand a little bit about what the colors mean. If we were to analyze this interaction, we can see here that we've got a click event. There's some yellow, which means there's some JavaScript being executed. There's some purple, which means there's style, some layout. And finally, there's a little bit of green, which means there's the paint update happens on the screen. So let's quickly jump into how we measure stuff. But before we do that, I want to quickly touch on this topic, which is we always want to measure against the production build. The reason for this is that development libraries such as React have coding and development build that isn't in the production build, which means that the code that users or customers may be experiencing isn't the same as what you would be working with in a development build. So always measure the production builds just to get a real sense of the numbers. With that being said, how do we measure? That's the first question we have to answer. That brings me on to the user timings API. And the user timings API is provided by the browser, and we can access it through window performance, which is an object. There are a few useful methods on window performance, such as mark. And what mark does is it creates a high resolution timestamp, and we can associate that timestamp with a name. We can then later access these timestamps using window performance measure. In this example, I've created two timestamps, one create mark start and the other create mark end. We can then later access these using window performance measure. And window performance measure creates a new timestamp, but it calculates the duration between a start and an end. Using window performance measure is how we visualize stuff in developer tools. OK, so now we know a little bit about frames. We know how to measure. Let's jump into the demo and start trying to create a baseline. So the first thing that we could do is if we think about the interaction that we were profiling, when we click that button, the state changes and the order of the cards update on the screen. So one thing we could do is we could create the first timestamp, which would be the start. Inside the on click event of the button. So I'll jump over to VS Code here and we can just have a look at the code. So this is the demo application locally. We've got the latest launch list component, which is the functional component that has the button and renders a list of launches. So inside here, inside the on click, let's mark the start. So that's the first thing we have to do. In order to mark the end, we have to use some instrumentation code. And this instrumentation code is just a custom hook that we'll use to store the value from the previous render inside the ref. I'll copy this custom hook. And the way that we will use this is like so. We have an effect that runs after render that compares the previous order, which we've used in the custom hook. And we compare it against the new order prop. If those two values are different, we know the state has changed and we can mark the end of that transition. So if I copy and paste this. You just put it here. That should give us something to analyze and measure and develop with those. So now let's switch back over to the demo. That's reloaded. So I'll just clear this recording. Press record again. And again, I'll just interact. I'll just change the order of launches a few times. Okay, so there's a lot of stuff going on here. Let's take this one, for example. As you can see, in this user timing section, we've now got a new measure. This is the measure that we've just added just to get some understanding of what the baseline is. So armed with the theory that we had for frames, we can obviously see that this combination of JavaScript style layout and paint is roughly 634 milliseconds for that interaction. Also, remember that we added instrumentation code. So we want to subtract the instrumentation code from that number. So this here will be that instrumentation code, around 58 milliseconds. So if we remove 58 from 634, we're roughly around 500, 550 milliseconds. So that is the kind of baseline that we're working with. So if we think about what happens during that interaction, we're changing the order of cards. So the question is, how many cards or launches do we have in the DOM? One quick way to do this would be to change over to the elements tab and look for the containing element. So if we look at this element here, this has got the list of launches. If I click those three dots, store as a global variable, it means that I can now access this element and I can read some properties on it. For example, child element count, which will give me the amount of launches that we've got. So what we're dealing with here is 102 items in the DOM. So this is obviously causing a lot of work for the browser to do every time that we do that transition. So how can we mitigate this? How can we fix this problem? That brings me on to the technique of virtualization. So virtualization, also known as windowing, is a way to efficiently render large lists of content. I actually personally think windowing is a better term for this concept. If we look at this image here from the WebDev article, which I highly recommend reading, this really illustrates the concept. So the user is only looking at maybe a certain amount of items on screen at any given time. But also there are some off screen items that the user doesn't see initially. Let's say, for example, the user likes to scroll fast. So the window would then change over a subset of that list. And it's an efficient way to kind of render content, like if you've got a large list. So before we apply this technique in the demo, I'll take a little detour around what we were doing at the zone. So we adopted virtualization at the zone. And before we adopted it, we were using a lazy loading approach of rendering content. And what you can see from this GIF is that when you're going up and down the content, we are just changing the window of what the user sees at any given time. The problem we had before was that as the user was interacting and going up and down the content, we were lazily loading content in and changing the DOM and updating new elements. The problem with that on low end devices is that it actually causes significant lag and significant jank in the user experience. So we wanted to remove that in order to provide a good user experience across all these different devices. And so virtualization really helped here. And this is just the vertical example. Here is the horizontal example without any animation. So as you can see, there's only maybe five items horizontally on screen at any given time. Off screen items, there's maybe two on either side. So if the user is going fast, for example, they have a remote and they hold down right or they hold down left, then the rendering is quite efficient because all we're doing is just changing the subset of the view. It's also worth speaking about the constraints on TV and how they're slightly different than the constraints you may be faced with on mobile web. For example, on TV, one of the big problems is focus management and maintaining consistent focus throughout different pages. Another one is the wide range of hardware specifications. For example, some of these devices have really low memory, low CPU and some have OK memory CPU. And also we're dealing with a wide range of browser engines. I think the oldest browser that we support is Safari 6, which is quite old. And obviously that means that there's a lack of modern web standards. At the same time, there's also lack of developer tools, which means that it's really tricky to navigate this landscape when working with performance. And when we're building this front end application at the zone, we're writing it once and we're running it everywhere across these different devices. So any technique that we use, such as virtualization, has to prove effective across all these different devices. Some data for the before and after virtualization, I took this profile using the Fire TV with playback and the interaction was going from the top of the screen to the bottom. So I was pressing on the keypad, going up and down just to get to the bottom of the content. And what we could see by adopting virtualization were a 34% reduction in JavaScript execution, a 43% reduction in style and layout, and a 59% reduction in paint. Just quite a significant improvement in terms of user experience. It was a lot less janky, interactions were a lot smoother, and we were able to maintain 60 frames per second in these interactions. So now we spoke about virtualization and what the technique is. Why don't we try and apply that to this demo, given that we've got a large list of content? The library that I'll use is called Masonic, and in the React ecosystem, there are a very wide range of virtualization libraries. The reason I chose Masonic for this is that it's really simple just to get started with as a render prop API, and we just tell the component what width we want and change the way that we render the cards. So I'll copy this. I'll just change the way that we render the cards. So if I move this up here. I'll save that, and I'll jump back over to the demo application on localhost. I'll just close the console. Clear the recording. And I'll press record, and I'll just interact with this a little bit more. And actually what I notice is it's actually faster. There's a lot less jank than what we were experiencing before. So let me stop recording. So let me zoom in on this one. We're now looking at about 97 milliseconds for that interaction, which is quite a drastic improvement from 550 milliseconds. So the application still works as expected. It's just a list of content. The only difference is if we look at the DOM, and we just inspect the containing element, what we're dealing with here is not 102 launches. We're dealing with one, two, three, four, five, six. So there's only some items on screen at any given time, but it's just officially rendered, and the DOM nodes stay consistent in terms of the count of DOM nodes. Okay, so that's quite a good improvement. I'm happy with that. The next thing we should probably do before we finish is measure the production build, just to get an understanding of what users may experience. So what I'll do is I'll just run the build command, and then I'll do npm run serve, which will start a local host on port 5000, I believe. So I'll switch back over here. Come to port 5000. And it loads the application. So I'll just clear this recording. I'll press record, and I'll just interact with it a bit more. So the images have just taken a little while to load. But we can see from the interaction that actually the DOM is updating quite fast. So if we just profile this here, we can see it's now taking around 46 milliseconds to do that interaction, which, given that we were doing around 550 on a development build, 46 milliseconds is okay. Virtualization did offer significant improvements in this case. There are further improvements that we could do. I don't really have any more time to kind of dive into those, but there are further techniques that we could use. So we've measured and analyzed. We've got a baseline, which is around 46 milliseconds, and we've measured the production build. So to conclude, really, what we spoke about is the measure, analyze, and fix cycle, how we can use that to identify performance problems. We've got the user timings API, which is just a way to measure. And I've introduced the virtualization technique, which is extremely useful for rendering large lists of content. And that's it. So I hope this was interesting. I hope you learned something. If you have any questions or comments, please feel free to reach out to me on Twitter and GitHub, at Richie McCall. And I hope you enjoy the rest of the conference. Thank you. Stay safe. Goodbye. Excellent. Excellent. Thank you so much for giving this talk and being here with us today for the questions. And I've seen that a few people are already quite active submitting questions. So let's get right into it. We do have a question. We do have a question from G Halpern, who's asking, in your demo, you used performance.mark to define different timestamps and then performance.measure to calculate the time in between. But what does performance.measure do when only given one parameter, which is the name? Well, performance.measures for a start and an end. So if you only give it one, you aren't really measuring anything. You've got to have some sort of start and end point to measure the code. So if you only give it an end, then you've got to ask yourself, what is it you're measuring? I mean, OK, that's a fair question. Also, we got a question from Mike. What is your opinion with all the performance hunting that you invested in and where it absolutely made sense? But what's your opinion on premature optimization? If you believe it's a problem, how do you actually then go about tackling it? I guess it comes in a few flavors, premature optimization. Like there's the case where you try and optimize code that you haven't measured yet, which is probably one of the big problems. Like I've said already a few times in this talk, it's all about measuring and getting the data in the first place. Once you've measured and you've got the data, then you can start optimizing stuff. If you feel like you're optimizing code before you measured, then how do you know what the improvements were if you never measured at the start? So I think always measure is really the key takeaway. Which brings me to a question of my own. I see that you have done this very specifically in the developer tools. But I've seen that there basically is a JavaScript API. So how would you go about monitoring performance on your site? Is there like tools that you can use? Is there solutions that are pre-made or do you have to tinker your own? Yeah, good question. So in performance, the tooling falls into two main categories for me anyway. So you've got lab tools and you've got Rum tools. And so lab tools is something like say Lighthouse or React Profiler or DevTools, for example, which is testing the performance in a very constrained environment that you control. And what I mean by that is like you can change one variable and see how it changes and keep doing that until you've got something that may be working. That's the lab environment. For a Rum environment, which is like real user monitoring, there's a few tools that you could do. There's what I've shown here is the performance API on the window. But with that, you have to be careful just in terms of browser support. So for us on TV, it's kind of not the best given that we support really old browsers and they don't all support the performance measure API. So that's something to be aware of. You can do that if you're only supporting modern browsers. But there are other tools. There's like a tool called Speedcurve, which is quite good. That's like a really robust like one monitoring option. But obviously it does come at a cost and there are similar products out there that do similar things like New Relic supports kind of performance monitoring on the front end. And also Sentry does the same. So there are a few options, but really it's lab and Rum monitoring. Right. Right. So you have to basically find a strategy that optimally involves somewhat both sides of that, I guess. Also, Ojo is asking, is it possible to somehow combine virtualization and SEO friendliness in the page? I want less work on main thread, but Googlebot should see all content on the site. It's a very good question. To be honest, I don't really have any experience with that and trying to make that work with the kind of the Google JavaScript crawler. I don't know how robust a JavaScript crawler would be to something like virtualization. Maybe somebody out there already knows the answer to that. That's quite a good question. It's worth investigating. I think that question might also be pointing in my direction because I actually work in the Google search relations team and I am very familiar with Googlebot and its JavaScript crawling abilities. There's multiple ways. One way would be to dynamic render, which means render or pre-render or server-side render a version with all the content just for Googlebot. That is acceptable as well. Also, if it's on the main thread, if it's off the main thread, we should be able to see at least some of it. Do check it in the testing tools to see if we are seeing your content and alternatively, you would have to find ways of presenting the content to Googlebot like dynamic rendering, server-side rendering. It is possible. It just requires a little bit of testing to make sure that there's no pitfalls and surprises. I know that our web worker implementation isn't perfect, but bringing it back to the audience, Layard asks any suggestion on how to do automatic instrumentation. I think we kind of covered this, but do you have any other things that you want to say on that topic as well? If you're in a lab environment and you want to use the window performance API, I noticed that Lighthouse recently started including these measures inside the audit itself. That's something you might want to take a look at. I noticed that recently when I was trying to run Lighthouse on a daily schedule against some of the TV applications that we've got, and it was hitting the authentication page, the sign up and sign in, and I noticed that they still had some of the React timings on the Edge environment. I was like, that's strange. It seems as though some of these performance measures are actually showing up in Lighthouse. But that was also how I realized that they were shipping the wrong build to that environment. So, two surprising issues. That's cool. So you can run Lighthouse in a scheduled way, automated as well? Yeah. I'm actually working on a blog post just now, which is setting up a serverless Lighthouse reporting, so you can just run it on a schedule and get a little notification that says, hey, your performance is 98 or whatever. Lighthouse is really good. I'm really impressed with Lighthouse over the last few releases. Also, your talk was quite hands-on, and that's amazing. But there is a question from Bri that I kind of came up with in my mind as well. I had that as well. What additional resources would you recommend for someone getting started in analyzing and improving performance? Because we've talked a little bit about how you can automate this and how you can do this in a hands-on way. But what resources would you recommend to someone who's just starting out and trying to understand what they're looking at? Yeah. So I guess the first thing before you even start looking at trying to fix any performance problems is getting the data in the first place. So it depends on how you control the environment. But there are tools like, say, WebPageTest, which offer the option to do bulk testing. So that would be useful for getting data in the first place. And really what you want to do is once you've got these kind of key data points, is start trying to analyze the data and break down where the problems might be coming from, like what pages are the performance coming from. Because really what you want to do is have a performance problem in the first place, because you don't want to start trying to improve the performance if there's no problem. For example, if you're working in a business, you have to make sure that this performance problem is impacting the bottom line for the business. So you have to get data points and then work out where the problems are and then take that to the business and go, hey, the performance problem's here. I propose that we do this, this, this, and this, and we can try and fix it. So that's how you approach it from a business point of view. From a developers point of view, trying to get maybe involved in and just understanding how to do it. And there's lots of great resources like Google is probably the best place for any performance resource that I've found in my career. And like the Google Summit that they do, the Chrome Summit, like all the talks that they do every once a year, there's always something new to look at from those talks. So I would recommend that. Cool. Also coming into the avoiding performance problems from the get go, Anna is asking, are there any good habits which we would recommend someone new to React to build into their workflow to help them basically have good performance in the long term and not run into performance problems in the first place? I guess in a React point of view, really, you're typically measuring either the page load time or interactions. So page load time, you can kind of do that whenever you want, really. If your page loads fast and there's nothing to complain about. In terms of working in React and working on performance as you go, I would say think of the interactions that you're building and then just try and understand what's actually happening under the hood, I think is quite useful. Like a lot of people say that you shouldn't have to look under the hood, but I actually disagree with that. I think if you're talking about performance, then you do have to kind of dive a bit deeper and understand interactions and what goes on. And like I say, always measure a production build. So don't measure like I've done in the demo, just measuring the development build, always measure the production build. Speaking of production build, that's an interesting question from Matthias. How or do you actually measure the performance of your apps on customer TVs as well? I mean, you want to be in the production environment as much as possible, and that would be customers' TVs, right? Do you do that? Yeah. No, we don't do that. But it's something that we're looking at more not in a real user monitoring point of view, but more in a lab. So we've got a test device lab with all these different devices. And what we want to do is start analyzing interactions in a lab environment, but it's also a real device environment. So it's a bit of a kind of hybrid approach. But in production, no, because like I say, some of these browsers don't support some of the stuff they were trying to do. They can model web performance, marketing, measuring. So it's just a bit too risky for some of these environments, given that it's kind of build once and ship everywhere. Yeah. Yeah. Fair point. One more question from Spidey, or Spidern. Isn't 100 elements from your demo, isn't that still a very small number of elements to need virtualization? I feel like we are solving most of the mistakes we do by virtualization until it's not enough. Do you think that's a good practice? Well, I mean, the demo is really just to kind of illustrate the concept. Maybe 100 elements. I mean, it depends. If you're thinking about a Moto G4, like a really kind of really standard average phone, then if you've got 100 React components on screen at any given time, and you're trying to change the order of them at any given time, you're really putting the browser under a lot of strain. So virtualization just solves that technique of only having, say, six items in the DOM, which is actually a lot more stable in terms of CPU and also with memory. So I mean, I guess really what I want to do is measure. You wouldn't know if 100 elements is a lot or too little unless you measured. And from what we've seen in the demo, even measuring on like a kind of modern laptop with six times slowdown, the browser was going through a lot just to do that update, and it was quite janky. So I imagine what that would be like on a lower end device. I see. All right. Thank you so much, Richie. I think we should head over to the mentee, Paul, that you gave us and see what the audience has said there. So let's check that out real quick. Also, thank you again. Thanks. Thanks again so much for A, the fantastic talk. I think we're not seeing enough performance talks, to be honest, because performance still is a big issue. And also B, for all the Q&A. I know it has been a lot, and I hope that you have a fantastic day. Thanks a lot for coming. Perfect. Thank you, Martin. Enjoy the rest of the conference, everyone. Thanks.
34 min
17 Jun, 2021

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic