The Amazing World Of Browser Performance – How To Be Performant

   Tools
Browser Performance Feature Image

Software developers all hear the word “performance” at some point in their careers. We know we want our applications to be performant, and we all have some idea of what that may mean and how to achieve it. That said, it’s a good idea to define what performance (and consequently, performant) means.

So, What Is Web Performance Then?

According to dictionary.com, “performance is the manner in which or the efficiency with which something reacts or fulfills its intended purpose”. This can, of course, mean a whole lot of different things depending on what you’re trying to analyze. When applied to a browser’s point of view, we can define performance as the effort performed by the browser to render an application and show it to the end user. In the end, an application that’s performant will be one that can promptly respond to any of the user actions, so that the user experience is fast and seamless.

Why Should Developers Care?

In an era of super-fast networks, high-end multicore processors, cheap storage, and cutting-edge frontend frameworks, performance is a feat that has lost its importance for most developers. Our lives and jobs nowadays move so fast that optimizing tasks is something very few think about. While optimization is often taken care of by the frameworks we use, it’s important for a good developer to understand why something is (and should be) performant. We need to understand how the rendering pipeline works and how to optimize it if needed. We developers are so busy developing intelligent systems that we often forget the end users are humans. Thus, users should always be the focal point of any performance effort so let’s look at some numbers:

Performance - Chart

By analyzing this data and considering that the end game is to keep the user focused on our application, we can see a couple very important pointers:

  • We have from 0-100ms to respond to any user action so the user has feedback from that action. If we’re communicating with our backend, this doesn’t mean that we need to have all the content loaded and shown by this time. We need to provide some feedback, so the user knows he/she performed an action. Any animation, message, or transitions will let the user know something’s happening.
  • We have around 1 second before the user loses focus and probably drops the task he was trying to accomplish; this means that, if we need to load something for the user to complete this task it should load in less than 1s or faster.

If you want to read more on perceived performance, here’s a great article by Luke Jones.

Know Your Tools

Before we dive into performance analysis, we need to know and understand the tools we’ll be using for this. For the purpose of this article, I’ll be using Chrome’s DevTools to analyze performance (and other things) but you can consider any other tools, like:

Here’s a view of the Chrome Devtools’ performance tab after you have recorded an event (make sure you have the “Screenshots” box checked):

Performance Tab

That’s a lot to digest in one small panel, right? So let’s break it down a bit:

1. Timeline view

Timeline View

The first and probably most important view you have in this panel is the Timeline View. This shows all the events overtime (time on the X axis.) We have 3 graphs that show all possible events:

  • FPS (Frames per second): shows you the frame count over time. The higher the bar, the better your performance is; ideally, you want to keep the count over 60fps to have smooth animations and transitions, anything below that will be perceived by the user as a janky transition
  • CPU: all CPU activity, color coded. We’ll discuss later what each color means.
  • NET: Network activity over time.

Aside from the graphs, you can narrow down the time window you need to analyze by dragging the small gray squares on the sides. Also, you can use the WASD keys to move around the timeline and zoom in and out while you’re navigating the Events view. The time window you select will affect the sections below. By hovering over the timeline, you can see a screenshot of the frame at that moment.

2. Events
Once you have a time window you wish to analyze, go to the events view and
see the activity in different areas of the rendering browsers activity:

Events

The main sections you’ll find in this panel are:

  • Frames: Shows you screenshots of every frame over time.
  • Raster: Rasterizing is the process the browser goes through to transform all the information passed by the rendering pipeline and converting into pixels on the screen, this section shows you the raster activity
  • GPU: This will show you any GPU event overtime.
  • Main: Shows you all the render pipeline events over time nicely set on a flame chart. Each color represents a different step in the pipeline; every event will show below it any action triggered by that event as you can see here:

Main

We can read here that the Idle Callback (the function that the browser executes every time it goes “Idle”) triggers a function call which triggers an anonymous call and so on.

We will spend most of our time analyzing the Main and Frames sections since these are the ones where we as developers have more control over what happens. This is where can identify and optimize most of the performance bottlenecks.

3. CPU Performance panel
Last but not least, at the bottom of the console we can find the performance panel with the following tabs:

  • Summary: this will show us a summary chart of which tasks the browser spent our selected time box doing. These colors correspond to steps in the rendering pipeline and consequently to the colors in the Main events section and the CPU graph in the timeline section.
  • Bottom-up, Call tree and Event log are all different ways to visualize the different calls made by the CPU. They’re all color coded and show you which event triggers what.

We’ll focus on the Summary tab since it’s the one that condenses the information most effectively.

Tip: If you’re trying to analyze performance on more specific cases, explore Settings by clicking the gear icon on the top right corner of the menu. You can find some very useful settings like network and CPU throttling to simulate different environments.

Be Performant

Now that we understand why it’s important to have a good performance in our apps and we know our tools well, we can now dive into how to build more performant sites.

Of course, there are a lot of things to consider when we want to create performant applications. Minimizing HTTP requests and pre-load/post-loading of certain components of your application, among other things. There’s a great list of best practices put together by the Yahoo! Team that you should read about. Let’s focus on understanding the browser’s effort to display content. We need to first understand the render pipeline, the process the browser goes through to render content to the end user.

JavaScript to Composite

1. Javascript: this refers to all your insert framework of your choice code and DOM interactions. All of this is executed before the new frame is rendered. For this initial step some things need to be taken into consideration:

  • Avoid Micro Optimization: It’s important to remember that Javascript code is not the final code that’s run by the browser. All browsers interpret Javascript code through their own engine (V8 engine in Chrome’s case). Each one performs their own optimizations so most of the time it’s not worth it to micro-optimize certain operations. We may end up making the engine perform more work than it should.
  • Run Javascript as soon as possible in animations/transitions: Because JS can make changes in any of the other steps in the pipeline, it can cause repeated steps or re-doing any part of the pipeline for one single frame. Considering we have to run the entire pipeline in around 16ms tops to keep up with our 60fps quota, we don’t want the browser to be repeating steps unnecessarily.
  • Optimize your code for a good memory management: Making your code garbage collection friendly will help your js code run faster and better. Here’s a great article on garbage collection friendly javascript that can fill you in a bit more on how it works.

2. Style Calculations: Every time a style is applied to an element of the DOM, the browser needs to perform calculations on a different thing. This is a two-step process that starts by matching selectors to actual DOM elements and finally constructing the actual RenderStyle out of the matched selectors. The simpler the selectors, the easier it is for the browser to find those elements and apply the styles. To keep your style calculations performant, you’ll want to make them affect as few elements in the render tree as possible. For this it’s a good idea to implement a naming convention and methodology like BEM (Block-Element-Modifier). This will not only improve your styles performance but make your CSS code easier to read and more maintainable. Also, it’s important to note that different CSS properties can alter different steps in the pipeline. A great resource to check triggers is CSS Triggers. The sole fact of being aware of what triggers what can lead you to write more performant CSS code. Additionally, to ensure the best chance for a smooth, high-quality animation, we should always try to use these GPU-friendly properties:

  • Transform
  • Opacity
  • Filter

3. Layout: Once the browser has calculated which style rules apply to each element, it begins calculating spacing and dimensions. Anything that involves spacing and dimensions changes within the DOM being rendered will trigger the Layout step. This includes resizing, moving elements around, and changing layouts. This step will be skipped if none of these actions are required, so it’s a great place for optimization and improvements. Always watch for layout calls as this is one of the most important steps of the whole process. It’s also very useful to make use of the new flexbox layout and to use GPU-friendly properties (transform, opacity, filter) to avoid any unnecessary spacing calculations and to optimize this step.

4. Paint: This is the process to convert all the information provided by the render tree that has been gathered through the previous steps into pixels. The browser goes through all elements and creates different layers with the elements drawn in them that will later become the end rendered screen. Just like the Layout step, if nothing needs to be painted the browser will skip this step.

5. Composite: In this final step, the browser puts together all the layers created in the Paint step and will render them on the screen. This process will give the user the sense of depth and will put all elements in the order they were designed. It’s important to keep as few layers as possible.

Whenever doing a performance analysis, we must keep the user’s experience as our highest priority. It’s a good idea to keep the RAIL model in consideration when measuring our performance. You can read more on the RAIL model on this article by the Google development team.

Let Us Analyze!

So, let’s take all this information and apply it with an example. I’ve created a simple example to help you detect potential performance bottlenecks. We can use a very simple React application I made, a simple map application that you can use to search for movies that were made in San Francisco:

Map of SF

The performance issue with this application is the speed at which the filtering options load. To analyze the performance, record the event in the performance profile. Here are the results:

Result

Immediately, we can see that there’s a bottleneck around 1.6 seconds in the timeline. If we open up our Main and Frames sections and narrow our time window down to the piece with the most activity, we can see that once the click event was fired, it took the browser ~1.6 seconds to execute the eventHandler that triggered the frame transition.

Main and Frame

You can view more details on the click event by highlighting it and viewing the summary panel. We can see a little more information on it and we can see that it’s taken longer than it should, so let’s do some more digging on what this Click Event is triggering:

Warning

By digging a little further down the call stack on this Click event we can see a react-click event, which is likely the callback for the Click on this component. We can see there’s a warning on it too (because it’s taking too long). Below it, there’s a callCallback function and three calls to a function call Sidebar._this.handleDropdown. Let’s look into what that is:

Warning Continued

If we click on that event we’ll see more information on it, including a line number to the code that’s being executed:

Clicking Event

Ah, so here’s our problem! Someone decided it was a good idea to do a lot of printing whenever the drop-down was clicked, thus causing our performance problems.

  handleDropdown = () => {
    if (this.state.searchTerm === '') {
      for (let i = 0; i < 1000; i += 1) {
        console.log(i);
      }
    }
  }

Let’s remove the problematic code and run our performance profiler again!

Performance Profiler

Now, that looks a lot better! Our scripting times have decreased considerably and most of the time the profiler was running the browser was idle. This means it wasn’t calculating, animating or rendering anything. We have optimized our application! While our example was extreme and you normally won’t have a function printing a number 1000 times the purpose of this was to show you what a performance bottleneck looks like and how to use the tools within the browser to identify and correct it.

Conclusion

After going through a small portion of what a performance analysis would be, we can point out a few things to consider:

  • Try to maximize the time the browser is idle. This time can be very useful to load heavy parts of your application, preload resources, or even do some calculations that will be needed later.
  • Avoid any changes or reads to the layout to avoid any forced sync layout calculations by the browser. Refer to the Google Developers Guide to learn more.
  • Well-structured and documented code is easier to maintain and optimize. Always try to write your code with the best practices.

Further Reading and Resources

To learn more on this very important topic, consider these in-depth resources:


This website uses cookies

These cookies are used to collect information about how you interact with our website and allow us to remember you. We use this information in order to improve and customize your browsing experience, and for analytics and metrics about our visitors both on this website and other media. To find out more about the cookies we use, see our Privacy Policy.

Please consent to the use of cookies before continuing to browse our site.

Like What You See?

Got any questions?


>