What can go wrong

Making big sites fast - @jtwb

Fork this post

In these early days of the age of rich web applications, the knowledge for building very fast yet complex interfaces is not well-distributed.

This article is about common performance bottlenecks you'll encounter in a large rich web applicaiton, possibly built using Ember or Backbone, and the awesome tools at your disposal for diagnosis.

Bottleneck: Renderer & DOM API Cache

In my experience, most performacne problems attributed to slow Javascript code execution are really problems with the DOM API and its cache.

Some background: the browser exposes this powerful DOM API to the JS runtime with real-time information about the position of all objects on the page. When you adjust the live DOM in some way, be it by inserting or deleting nodes, or updating CSS properties, the browser's representation of that positioning information becomes stale. Recomputing these properties is a very slow process compared to your fast JS code (1-20ms).

Fortunately the browser is lazy - instead of recomputing the layout on every write, it buffers writes in a queue. Ideally, the writes can be committed when your javascript execution terminates.

If you read a stale property, you incur a DOM API Cache miss, which triggers immediate evaluation of that write queue, layout algorithms and recomputation of positioning info.

DOM Cache Misses

Spotting the issue: Using the Timeline view, notice the interplay between the green Renderer activity bar and the Yellow JS activity bar. If you notice repetitive reflows during slow blocks of JS execution, that's DOM cache misses

Fixing the issue: Imagine each write-read cycle as expensive. Write-write-read-read is no more costly than write-read, but write-read-write-read is twice as costly. Try to batch reads and writes into blocks. If the app is using a front-end framework, the framework may provide a way to buffer DOM updates and commit them in one big chunk. Avoid badly timed reads (between writes) by memoizing data read from the DOM API.

Bottleneck: JSON & HTML Parsers

Parsing JSON is CPU- and memory-intensive. What you might not expect is that CPU exhaustion during parsing can occur and cause major sluggishness with seemingly reasonable datasets. Insidiously, these problems can evade development teams who are not checking their app on mobile devices or older machines.

Anecdotally, repeated parsing of 140kB strings of JSON causes noticeable sluggishness on my 2011 MacBook Pro.

Resource Exhaustion During Parsing

Spotting the issue: in Chrome Speed Tracer you'll see big heaps of sluggishness. Subjectively, phones are affected more and you may notice locked-up browser and possibly device interface.

Fixes: Do you really need that data? Consider a stripped-down version of the API with less data per-object and no unneccesary objects. Can you parse the data later or on-demand? It's OK to hold on to the raw responseText and parse on demand.

Years ago, GMail contained so much JS code (for the day) that a substantial amount of the load time was spent just parsing the code. Loading was not the issue - just parsing. Their solution was to transfer their code as modules in comment blocks and only parse a module when needed. So when you clicked (or hovered) the Compose button, that would trigger parsing of the composer module.

Bottleneck: User Events & Timers

Javascript runtimes are built around an event queue with no concurrent execution. It's very similar to libevent systems like nginx, EventMachine and NodeJS.

Innocently, a developer might attach an event listener to a high-frequency event like mousemove or scroll, or a frequent setInterval timer. Later, another developer or framework update increases the complexity of that listener's code. Suddenly you are in a world of pain and sluggishness.

Slow Listener Traffic Jam

Spotting the issue: Chrome Speed Tracer will mark these things as sluggishness. There will be a whole bunch of Javascript execution blocks squished against eachother in the timeline view.

Fixes: Poll for changes to high-frequency events like scroll and mousemove instead of listening. A high-frequency poll can often create the same illusion of responsiveness with far fewer runs. Within your callback, execute as little code as possible - especially avoid touching the DOM API.

Bottleneck: Javascript

"Slow Javascript" is usually blamed for sluggishness. This is rarely the reality of the situation.

Applications which work with Canvas, Video or large datasets can encounter CPU exhaustion or heavy GC delays from poor heap management.

Plain Old CPU Exhaustion

Spotting the issue: in Chrome Speed Tracer you'll see big heaps of sluggishness and excessively long processes. Subjectively, phones are affected more and you may notice locked-up browser and possibly device interface.

Fixes: Don't just guess at the root of the problem! Use the Profiler to identify exactly what part of your code is slow. From there, you have age-old techniques like memoization, taking expensive calls out of tight loops, flyweight objects, etc. You may also explore asm.js if you are truly, truly working with a very hot block of stripped-down code.

Bottleneck: HTTP

This topic is probably the most common source of slowness. It's well-covered elsewhere. Expert systems like Google PageSpeed and YSlow are very good at diagnosing problems in this area.

Browser layers and their respective diagnostic tools

Network tab (HAR viewer)
HTTP
Timeline
Renderer & DOM API
User Events & Timers
Profiler tab
JS Runtime
Speed Tracer
HTML & JSON Parsers

Further reading