Speed Tracer Examples

The following are some examples of using Speed Tracer to work through difficult to diagnose performance issues in Web applications:

Example Scenario 1: Redundant Layout

The Scenario

John is tasked with implementing the main table view of his company's employee performance review application. He dusts off an old table widget that he has used in the past and quickly gets a prototype working.

The table widget John uses exposes simple API for appending rows to the table. He uses this API and things seem to work well for tables with about forty rows. John's company has two hundred employees, and his requirements state that the table must not be paged. He quickly realizes that all is not well with his prototype. Using console.markTimeline() (see logging API), John sees that just rendering the table takes over 600ms.

John takes a look at the table rendering for a two hundred row table in Speed Tracer and notices an interesting pattern:

Speed Tracer tells him that there are lots of small layout passes that add up to nearly 80% of his total table construction time. He notices a repeating pattern in the trace tree of a DOM mutation: style recalculation followed by a small layout pass. John has fallen victim to the "Redundant Layout Problem".

The Redundant Layout Problem

The Redundant Layout Problem is a pattern of unnecessary invalidation of layout due to code that repeatedly forces a layout pass (like querying offsetWidth on an element) followed by code that invalidates the layout it just calculated (or vice versa).

Here is a simple example:

  // This line invalidates layout.
  elementA.className = 'foo';

  // This line requires layout to be up to date.
  var aWidth = elementA.offsetWidth;

  // Invalidates layout again.
  elementB.className = 'bar';

  // Requires layout to be up to date
  var bWidth = elementB.offsetWidth;

Modern browsers lazily compute layout so that they only do the work when it needs to do it. This means that the order of execution matters a great deal. The example above, when put in a tight loop, will end up doing more work than it needs to. We can clean this up by batching reads and style setting as follows:

  // This line invalidates layout.
  elementA.className = 'foo';
  elementB.className = 'bar';

  // This line requires layout to be up to date.
  var aWidth = elementA.offsetWidth
  // Second Layout pass not needed.
  var bWidth = elementB.offsetWidth;


John takes a look at the documentation for his table widget's appendRow() function and notices this description in the documentation:

  1. appendRow() is intended for incremental addition of a single row.
  2. appendRow() ensures that the column width sizes to fit the largest cell in that column, up to a maximum size.
  3. Table layout is fixed to allow users to implement column resize if needed.
  4. The final column is stretchy with its parent container.

John needs properties two, three and four, but property one gives John an idea of what to look for. John takes a look at the code and immediately sees the problem:

  var cellWidget = createCellWidget(getCellText(i));
  addCell(row, cellWidget);

  // John: This measurement requires Layout to be up to date.
  var columnWidth = headerCells[i].clientWidth;
  preferredWidth = Math.min(MAX_COL_WIDTH, columnWidth);

  // John: Setting the width of a column invalidates layout.
  headerCells[i].style["width"] = preferredWidth + "px";

In order to size the columns to the contents, the appendRow() function has to measure and update the widths of the columns on each invocation. To avoid repeatedly invalidating layout, John decides to build the entire table first, and then measure and set widths once at the end. It would have been even more performant for John to pick a default width for each of the fixed columns, and remove measurements entirely, but he felt that preserving the column sizing behavior was worth the extra time. He implements this function and calls it appendRowNoLayout(). John has in effect designed API for doing bulk table additions. See the results for yourself.

Speed Tracer agrees with Johns changes. The browser only has to do a single non-redundant layout pass for each column that gets measured at the end. Speed tracer shows John that if he decided to go one step further and use explicit widths for all the columns, he would save at most 28ms (45% of the 64ms) due to removing all measurement. Happy with bringing the table construction time down to 64ms, John calls it a day.

Example Scenario 2: Painting Pitfalls

The Scenario

In an attempt to keep up to date with the latest and greatest CSS3 has to offer, John decides to spruce up his company's awesome "text-blob-reader web application" with some animations and fade effects to give some feedback when transitioning between pages.

After an initial implementation, he notices that even on his fast workstation the transitions feel choppy and sluggish, and resizing the browser window makes the application stutter unacceptibly. If you have a WebKit based browser, you can see for yourself.

He first tries sticking timing information in JavaScript using "new Date().getTime()". Unfortunately all entry points into JavaScript yield zero time and tell him nothing. Then he tries profiling with Speed Tracer.

The Speed Tracer Sluggishness graph spells it out for John. Clearly the sluggishness is a result of poor paint performance, not poor JavaScript performance. John can now rule out the suspicion that heavy weight logic in the animation loop is the culprit. But what can he do about it?

What Affects Paint Time?

Paint time is proportional to the size of the surface area being painted and to the number of passes that need to be done to derive the final pixel colors. Here are a few of the things to look for when you see poor paint performance:

  • Overlapping layers with opacity.
    Having many overlapping DOM elements stack on top of each other with opacity can get expensive. Doing this means that the renderer has to draw all the pixels for each layer and is unable to prune hidden regions since it needs to sample color values from each stacked layer to determine the final composited color.

  • Gradients and box shadows.
    Gradients and box shadows require more than one pass to render.

  • Lots of text on top of gradients or mixed with layers that have opacity.
    Text is especially expensive to render when mixed with opacity since you need to composite multiple layers to render each glyph.


John takes a look at his code and realizes it contains all three gotchas. The first and the second are vital to the aesthetic he is going for, so he decides to work on the third one first. John remembers his clever trick for creating a pretty background using only text and CSS. It makes the download smaller (no need to download background images), but could it be responsible for his performance woes?

To test his theory, John comments out some opacity in his CSS:

  .layer {
    /* SLOW!
    opacity: 0.9;

He also comments out the code responsible for constructing the background:

  /* SLOW!
  function makeCoolBackground() {
    var backgroundContainer = document.getElementById("coolTextBg");
    for (var i = 0; i < 14; i++) {

When he refreshes his application, the animations are much smoother. John refreshes the page and sees that Speed Tracer approves of the change:

Voila! The UI is now painting snappily. Using Speed Tracer, John verified the problem was poor paint performance, and was able to quickly validate changes he made to the code in real time as he refreshed his browser.

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License.