Go Fast or Go Home: The Process of Optimizing for Client Performance
Increasingly there are a lot of signs that indicate customers hate slow pages. According to data from Google and many other sources, customers leave a page or become frustrated if loading page takes longer than about three seconds.
In the beginning of 2016 the homepage for Expedia took up to 8 seconds to load for some customers in some cases. Our platform had evolved to serve a variety of complicated needs for our global and multi-branded business. Speed, while important, had not been made a top priority. Once our team completed the global replacement of the previous platform it was time to focus on performance and greatly increase the speed of our pages.
Our target was a two second page load. Getting to two seconds has been anything but easy or quick. It has been a long journey over a year and a half. This blog is condensed walkthrough of changes the team made to deliver a fast performing page, and some insights about how we keep it there.
Expedia Homepage Performance Optimizations
Our very first step (for performance as well as development reasons) was to migrate our home and launch pages out of our legacy shared application to a standalone AWS application. Serving the pages from a smaller, lighter application closer to the customers immediately improved our 50th percentile (TP50) page usable to 2.8 seconds, but we knew we could be a lot better. The performance improvements were focused on 2 broad areas; Google Page Speed Index improvements and lower page usable times.
Measuring: Page Speed Insights (PSI) and Page Usable metrics
PSI (Page Speed Insights) is a Google tool used to measure the performance of a webpage for mobile and desktop breakpoints. PageSpeed Insights checks to see if a page has applied common performance best practices and provides a score, which ranges from 0 to 100 points.
Our plan was to implement the PSI improvements as recommended by the Google Page Speed Insights. The following table chronicles changes in Homepage PSI scores over time, highlighting both changes that we made to improve performance, and some drops as new features from our team and other teams were rolled out onto the page.
|Release||Mobile PSI Change||Desktop PSI Change||Implementation Changes|
You can see that there was not a straight-line progression; other teams also were adding features to the homepage and were not necessarily aware of our goals, which in some cases decreased our performance. This highlights the importance of communicating and enforcing time and performance budgets on large projects.
Page Speed Recommendations
Let’s walk through the main page speed recommendations we focused on and the implications of those changes.
Inlining Critical CSS
There are three ways to load and apply styles and css to a page:
- Load synchronously
- Load asynchronously
By default the css included on a page via a style tag:
Inlined CSS will not block the page from rendering. Of course inlined CSS suffers from a variety of other problems. Redundancy in markup can result in larger page sizes, which increases the number of packets required to download the markup. The browser cannot cache the css of the page. Finally declaring styles inside markup is also notoriously difficult to maintain. For that reason it’s difficult to recommend inline CSS for much.
Loading css styles asynchronously will not block rendering or impact page usable. However, a user may see a flash of un-styled content (FOUC) in the time it takes for the CSS to load. To prevent a “FOUC”, inline the critical CSS of the page and load the rest of the CSS resources asynchronously in the background. ‘Critical css’ is a term that refers to the CSS for above-the-fold content that needs to be in place.
A node module called critical was used to generate the critical css, and a proxy tool called Charles was used to test the quality of the generated CSS. We used these tools to block the css resources from getting downloaded, which let us verify that the inlined css assets were good enough to prevent the flash of un-styled content.
Eliminate Render Blocking Scripts
There are two ways to achieve this, with either async or defer attributes on script tags. In both cases the script is downloaded in parallel while the DOM is being parsed, but the way they are executed is different.
Async only pauses DOM parsing to execute the script rather than during the fetch, while defer waits until the entire DOM has been parsed before it executes the script. Defer also executes scripts in the order in which they are present in the DOM, whereas async provides no execution guarantees. This article has good recommendations on when to use which element as appropriate.
Leverage Browser Caching
Downloading resources from the server is both time consuming and expensive. If a resource has been cached in the user’s browser, it saves the extra roundtrip time to download it. This is especially useful for large files which could require multiple roundtrips. PSI recommends that static resources be cached for a minimum of one week up to one year if they don’t change often.
Gzip compression for Scalable Vector Graphics (SVG)
Modern browsers support compression for all HTTP requests. Resources can be compressed by up to 90%, which significantly reduces the time to download and render the page. Smaller downloads also reduce the load on the customer’s mobile data plan. On the Homepage, SVG files were minified and compressed at build time using gulp-svgmin and gulp-gzip plugins.
Image optimization using Thumbor
An internal team had just launched an image optimization service powered by Thumbor. This service helps to serve images faster by allowing images to be compressed, cached, and intelligently resized, then served by Akamai or cached in the user’s browser, without the overhead of manually processing all the images before hand. Even though it requires additional processing time the first hit, it provides for a faster experience overall without exploding the maintenance requirements the image library.
Our other metric: Page Usable
Google Page Speed represents a series of optimizations that can be made, but how do we measure the real browser performance and more importantly its impact on the customer experience? We can start by observing and considering the real world rendering flow until the page is usable for the users.
It is out of scope for this article, but we have a logging system that tells us in aggregate how well customer’s browsers are performing. We can slice and dice this data by client type, which led to our starting hypothesis about mobile performance. Our hypothesis was that mobile users are even more sensitive to page load times, so we focused on getting them to two second performance first.
History of Page Usable scores and progress:
|Version||Time to Page Usable||Changes|
Reducing the HTML of the page reduces the amount of data that needs to be downloaded down the wire and speeds up parse times. We looked for areas to optimize and reduce the HTML size, such as eliminating markup not required to render the initial load (50% reduction) and templatizing elements on the page (10% reduction).
As a result of a previous feature implementation, the Homepage contained the markup for the home tab as well as all the launch pages like Flights, Hotels, Packages, etc.. After eliminating that extra markup, homepage only contained markup for the Home tab. The tradeoff was that if the user clicks on a different launch tab such as flights or hotels a new request will be made. The improved speed of home page rendering more than made up for the loss of that “instant switch” feature. That feature was previously a winner in an A/B test. This is just another example of needing to continually challenge assumptions, as the environment changes frequently.
For templatizing elements, instead of including the markup for all rooms on Hotels, Flights and Packages line of business, the page contained the markup only for the simplest version of the search wizard. As the user interacts with the wizard and requests more complicated searches, markup is added/removed using jQuery. These changed helped to shed 2-3 hundred milliseconds off of the PageUsable time.
|Baseline||Page Usable (mobile tp50)||PSI (mobile)|
|Original Site on the legacy platform||7s||60|
|Improvements||Page Usable (mobile tp50)||PSI (mobile)|
|Migrate codebase to standalone AWS application||4s||58|
|Rewrite main search wizard with a custom, fast rendered version instead of the common version used in all contexts.||3s||58|
|Reduce use of server side library JSP tags in favor of plain HTML.||2.8s||58|
|Remove unseen markup previously used for instant tab switching||2.6s||58|
|Inline critical CSS, and lazy load lazy load the rest||2.5s||74|
|Templatize elements on page, reduce page size by 10%||2.4s||74|
What did we learn?
As you can see from the sequence, many of these changes didn’t individually make a huge difference on their own. It is an extreme challenge to optimize a page that provides a lot of business value and has many business interests competing for features and placement, so you must take the performance gains piece by piece where you get them, rather than only searching out big wins. Setting a strong time budget goal, measuring it, and making sure that everyone understands the goal is just as important as the technical means you use to get you there.