Letting People in the Door. How and why to get 2s page loads.

Bobby @ fiskal.app
10 min readOct 23, 2015

--

If you like this article, check out my work to solve personal finance at fiskal.app.

The main benefit web has over native apps is it’s speed to deliver content. Within a few seconds of entering a URL into the browser, users can get information about our product and sign up for an account. But if you’re not conscious of the user’s overall experience you’ll lose users and revenue. At Capital One’s Level Money team our new web app (https://levelmoney.com/app) started with performance budgets to keep the user’s perspective a top priority.

TL;DR — Webpack config setting are at bit.ly/megatome

Why 2 seconds? Because users change their habits when longer than 2 seconds.

Having a 2 seconds page loads is the target. This is not because of an engineer’s preference but because page loads slower than 2 seconds has a negative impact on users. Case studies with Bing, Google, Amazon and others show that page loads longer than 2 seconds start impacting the user. As engineers and designers we are here to provide value to the people that come to us. One key point about our 2 second page load time is that it’s independent of the connection speed. Most people do not live in downtown New York or San Francisco. Most people in the US are either on 3G all of the time or their LTE connection drops to 3G speeds about 40% of the time. If you look at the standard distribution of user connection speeds, we want the target to hold true for 75 percent of our users. After researching current mobile vs desktop usage and US network speeds, 75% of users will have a 3G connection or better. That gives us a maximum network load of 400KB to everything to render the page (there are other factors as well but we’ll dig into that in a bit).

“Most people in the US are either on 3G all of the time or their LTE connection drops to 3G speeds about 40% of the time.”

Real user-focused metrics.

When we talk about page load we are do so from a user’s perspective. This means from the time the person hits “go” in their browser until useable content is rendered on the screen. A more precise term for this is Time to Interact (TTI). We’ll also be tracking the total abandonment rate; the percentage of user who never complete the first page load. This is tracked by recording a timestamp when the server receives the first request until we’re painted useful content on the screen.

Agreeing to agree. Users first and empowering designers.

When we first kicked off building our web app for @LevelMoney, we wanted to set proper expectations with the designers and stack holders. Just a single image can easily take 400KB. We don’t want to limit the designer’s freedom or sacrifice the UI just for faster page loads. Web fonts are largely hated by developers because of their increased page weight and lack of control over loading and caching. But I love fonts; they offer so much richness for users, so let’s do it right.

“I love fonts; they offer so much richness for users, so let’s do it right.”

For web fonts, we agreed to only use open source fonts. When you use a paid font from a font shop they enforce all kinds of rules around loading and caching that can degrade the user experience. By using open source fonts we are allowed to inline them, cache them forever, load subsets or the entire font. This affords us lot of options to deliver a better experience.

The next agreement was to allow fonts to fallback to system fonts if needed. Fonts have always had fallback rules. It’s a amazing feature but unless you get agreement up front from the designers they’ll want everything pixel perfect. Now we can decide that if and when to use system fonts. When the user’s connection is too slow we can ignore web fonts all together.

The last agreement on fonts was to minimize the variation of font weights and font families. Too many font weights and families will add delays to network loading and rendering. If a page has a font style that’s only used in one place it will block the entire page while it’s downloading. By discussing these trade-offs up front and by having continual collaboration we can focus on what’s best for the user.

Picking the right stack. Techno mumbo jumbo.

We started with ReactJS; a fabulous, life changing library that is unparalleled in terms of developer friendliness, page load speed and very performant at maintaining a good frame rate. React’s immediate mode (aka functional transforms) allows for operations to be add on top of or side-loaded without increasing the complexity or interleaving of processes. We can do things like drop into any state of the application and tab through every state. After 10 years of using everything from PHP to Rails to Backbone to UIKit, React is mind blowing.

“React is mind blowing.”

Webpack as a support for React is just as amazing. It’s hot swapping code takes iteration cycles to be almost instantaneous. It’s nothing like browser watch reloads. Think of it more like changing parts of a car’s engine while driving down the freeway. The hot swapping code allows developers to stay in their editor and almost never need to touch Chrome Dev Tools or the browser. It’s greatest feature is it’s ability to separate javascript into different files based your code base. At Level Money we have small (~10 lines) build configuration files to switch Webpack between dev and deploy modes.

The third leg of our tri-force is ClojureScript. ClojureScript brings full functional paradigms to the browser. At it’s core it takes a single object and transforms it effectively and simply without introducing branching or complexities found in OOP languages. It’s asynchronous library core.async makes asynchronous work so simple and easy to understand. It’s been incredibly reliable with less than a handful of Clojurescript bugs during the build out of our web app.

Getting the biggest bang for the buck. Prioritizing optimizations.

What techniques will be the biggest impact on page load times for the least amount of work? We’ll start by removing all of the optimization in order to set a base line. All of our images for the base line have been properly sized to render on a Retina MacBook and are pre-optimized with TinyPNG. The baseline for our app is 4.2MB. Once all optimizations have been enabled only 301KB is needed to fully render the web page.

“The baseline for our app is 4.2MB. Once all optimizations have been enabled only 301KB is needed to fully render the web page.”

Most of these optimization steps are fairly common but there are 2 items to call out. For responsive images we’re selecting an alternate size of the same image that is no bigger than what needed to rendered on the user’s screen. The placeholder image technique that we use is a little more unique. Inside the HTML we have inlined a tiny image that is only 20 pixel wide. Then we stretch it’s width to 100% and put a heavy Gaussian blur on top. This will give the perception that the page is fully loaded sooner. Once the placeholder is rendered, we will download the responsive image and cross fade between to two. Using a placeholder image isn’t appropriate for all images but with big, full-bleed hero images it works beautifully.

Mission Un-Accomplished

Now with all of the optimizations, we’re 25% below our page weight target (even with 40KB of fonts). But in testing the page takes longer than 2 seconds to render. For the next phase we need to look at the overall system. What work is being done and what is the order of operations?

To simulate real-world usage over 3G, we’ll use Chrome Dev Tools. In the Network tab we’ll turn on screenshot capture feature (the video camera icon). The first paint here takes 2.3s but there’s no text. This is utterly useless for the user so we need to find the first time text is render, which is at 2.83s.

First Paint 2.3s
Time to Interact 2.83s

When we pull up the HAR files, the first thing we notice is that the web fonts don’t start loading until after the all the JavaScript is downloaded and executed. This is because browsers won’t download a font until it’s used. In order to fully utilized the network, we’ll need to force the font loading sooner. We’ll include our font-face CSS rules in the index.html. Next we’ll add an element to the page each font. This tells the browser exactly what we want. Once we force the font loading, we’re now rendering the page in 2.64 seconds.

Parallel font loading Time to Interact 2.64s

If we wanted we could remove the web fonts when on 3G connections. Web fonts add a lot of richness so removing them should be a last resort. The goal is to balance a great user experience with a speedy UI. If we removed our web fonts it only saves 0.4s. The impact is minimal and they’re better options to increase page speed.

The road ahead. Doing as little work as possible.

We’re happy with our current page loading performance but have a few more optimizations in the pipeline. When looking at the loading graphs there’s a 0.4 second break when JavaScript is compiling and processing our code. We also have bluebird.js included in our ClojureScript that add 20KB. The only supported browser that doesn’t have native Promises is IE11. Setting up bluebird to be an optional download will speed up all other modern browsers.

In order to get below the 2 second target we’ll need to do a lot less work. Downloading, compiling and running our Javascript in the browser is the biggest factor to our page load. In reality we don’t need any JavaScript in the browser to render the first page. We only need HTML and CSS. React has a wonderful feature called immediate mode which affords us options that aren’t practical with other frameworks. Immediate mode is a stateless transform from raw data into HTML and CSS. We can render the HTML and CSS server-side and send it to the browser to render directly. It’s only after the page is render that we’ll download the Javascript and React will attach itself on top of the HTML that was rendered server-side. When look at only the HTML, CSS and an inlined placeholder image the total file size drops to 25KB (GZIP). This is where Webpack and React really shine. It’s only because of these tools that we can do this without introducing much complexity. This will take page load times down to 1.29s!

Dynamically server rendered HTML Time to Interact 1.29s.

Our server can support a larger amount of users because most of the CPU time is spent on the clients. During the build out of our first version we’ve incurred some tech debt so this feature isn’t currently enabled in out first release. We ran these number doing a static trial to inline the HTML, CSS and fonts followed by an asynchronous downloading of our JavaScript. Once completed this will allow our users to go to any page and any state of any page in about 0.8s!

Pulling back and focusing on the user.

After all this, will 800ms make a difference for users? Will these simulated times hold up with real-world use cases? We will be tracking our abandonment rates, real-world load times and A/B testing features of the web app. We can also start gathering metrics to see if we went too far. At what point do faster page loads have no impact on user behavior?

With page loading behind us, I’m excited to start tackling the really fun items. The things that have bigger impact the complete user experience. A few items we’re looking at are the following.

  • Better animations to provide context to the user and seamlessly integrate the experience.
  • Offline mode (w/ service worker) to allow the app to work better in “Lie-Fi” mode.
  • A/B testing our messaging to make sure we’re providing value telling people want we provide.
  • Building and user testing more features to rapidly iterate our app and create a better product.

Webpack config details are at bit.ly/megatome.

Our numbers for US network speeds can be found at http://opensignal.com/reports/state-of-lte/usa-q1-2014 and http://www.clickz.com/clickz/column/2388915/why-mobile-web-still-matters-in-2015. Page loading case studies are here: http://www.guypo.com/17-statistics-to-sell-web-performance-optimization. More details from my public talks are hosted on github at http://puppybits.github.io/talks.

--

--