Improving the perceived loading time of the new Trade Me site

Elwyn
Trade Me Blog
Published in
6 min readAug 1, 2017

Trade Me is an online marketplace and classified advertising business where Kiwis buy and sell all manner of items or look for jobs, properties or vehicles. Recently, we have been building a new single-page app to replace our old ASP.NET front-end.

The first version of the new Trade Me homepage viewed on a mobile device

Our first iteration of this was built using AngularJS, and unfortunately it suffered from performance issues.

The next iteration, still under development, is being built with Angular (the latest version of the framework) which is considerably faster.

Improvements in the framework such as smarter change-detection (i.e. how Angular knows data has changed and the page needs updating) and ahead-of-time template compilation (avoiding one of the expensive up-front tasks a JavaScript framework normally performs in the user’s browser) make the site much faster. In addition we’ve changed some earlier decisions around the data we fetch up-front, which has led to a much snappier experience.

Beyond these actual performance improvements, we’ve started looking at tricks we can do to improve the perceived loading time.

Avoiding the router “resolve” feature

In our previous iteration of the application, we used a third-party router library for AngularJS called UIRouter. It had a feature we used a lot called “resolve”. This allowed developers to specify all the API calls and data needed for a particular page up-front. This is convenient for developers because the page always has all the data needed by the time it runs.

Unfortunately this has a negative impact on our users, in that clicking on a link does not immediately navigate to the new page or show feedback until all the data has finished loading. At this stage, the whole page will render all in one go.

User testing showed our members frequently got confused about this, and would end up clicking multiple things because they thought they had mis-clicked or the site hadn’t picked up their action.

While the new Angular router supports this feature, we have removed all but one usage of “resolve”. Now the router immediately navigates the user to the new page as soon as you click on a link, which can deal with its own loading state in an appropriate way.

Everything has a loading state

Instead of a single loading spinner that shows the whole page as loading while individual API requests come back, we’re building a loading state into each component that makes up the page. This allows the critical content to be rendered as soon as it’s ready without being blocked by any extra requests the page is waiting for.

For example, we want our members to be able to read information on an auction they’ve clicked on as soon as possible, so don’t make them wait while we load in a “more stuff you might like” widget.

Rather than displaying spinners all over the place we’ve chosen to “grey box” the component while it’s loading.

“Grey box” product cards (aka “ghost cards”) shown while data is loaded (artificial delay in place!)

This gives the perception that the component is already partially loaded, and blocks out the rough shape of the page - meaning there is less jumping around as pieces of the page finish rendering.

Better use of data

Instead of showing grey boxes where data will eventually be, sometimes we can do one better and show some data immediately, while waiting for the API request to finish.

When a member is on a search results page, and clicks on a particular product/listing, instead of waiting for the full product details response from the API we immediately navigate and populate the product details page with the title, subtitle, product images and other key data we already had on hand from the search result. In most situations the API response for the full details will finish (and we populate the rest of the page with the new data) before the member has scrolled past the title and photos.

Reusing the search card data when transitioning from the search results to the details page allows for a snappier experience. Artificial delay added to clearly show the gray boxes used for prices to ensure stale data is not displayed, and for description, which comes back on the full details response.

This does raise some challenges around invalid data — as Trade Me has live auctions, the product price can change rapidly. So while waiting for the full details to load we “grey box” the price rather than using the cached price we had from the search results.

Local cache

Taking the previous example one step further, we’ve designed our applications data structure to make it easy to keep a local cached version of much of our data. We’re using @ngrx/store (a reactive redux implementation for Angular) to manage client-side data, which makes it easier for us to maintain a local cache of data.

An example of when this is especially useful is when a member is refining their search, and toggles a parameter on and then off again — as we’re doing a search for each parameter change we will have a cached copy of the previous results locally so when our member toggles the parameter back off we can instantly render the items without an API request.

Using the locally cached data to re-render previous searches instantly. Here you can see the loading state is only shown the first time the toggle is changed. This is also useful when clicking from search into the details and then going back to the search results — the results are instantly rendered.

Angular Universal

So far the tricks discussed have been about improving the perceived loading time once the application has booted, however there is still the initial load of the app. Single-page apps need to load data and do a bunch of bootstrapping up-front and therefore often struggle to provide snappy first page loads.

Angular Universal is a piece of the platform that allows the Angular application to run on the server. It can pre-render the requested page, and return the complete markup, with all content, to the user. Then it silently downloads and bootstraps the app in the background.

This is great for users, as most of the time the app will be fully bootstrapped by the time they have finished taking in the initial content.

We are still considering how we want to use Universal at Trade Me. As an example we may wish to only pre-render the main content of a page, and leave the supplementary sections “grey boxed” until the app has bootstrapped on the client.

Having our application data in a centralised store means we’ll be able to easily re-hydrate the application on the client into the same state the server got to with its pre-render.

Universal can also pre-render static parts of the site at build-time, for example the “shell” of the application. This can then be used in conjunction with Service Workers.

Service Workers

Service Workers work by installing a script in the users browser that runs separately to the main application. It can run when no browser windows have the site loaded, can intercept requests made by the application, and more.

This is very powerful, and will allow us to instantly show the pre-rendered app shell when the page is first requested — before the request even hits the server and Universal starts doing its thing.

Spike on an “app-shell” implementation. The initial page load shows a spinner (hardcoded into index.html) while the app bootstraps. When refreshing the page after the initial load (and the service worker has installed), an “app-shell” is shown immediately.

We can build in aggressive caching rules for data we don’t expect to change frequently (such as our catalogue/categories data), which will speed things up and save data on our members mobiles — requests for this data won’t actually hit the network.

In conjunction with our centralised state management with @ngrx/store there are some interesting possibilities. For example polling/loading of data in one tab can be shared between all running instances of the application.

Service Workers also open the door for providing an offline experience — for example a member out looking at open-homes might not have network, but we already have their watchlisted properties cached, so why not show them the address and times for their property viewings?

The future

It’s exciting to be tweaking this aspect of the experience, and there is lots of work still to do. So far the emphasis has been on building the mechanics to support these techniques — our design team is yet to bring their wisdom and apply some polish (our “grey boxes” really are just grey boxes!), and we have not yet user-tested the new experience. There are also many opportunities we have not yet explored, such as optimistic UI updates.

Hopefully the new Trade Me lands in your browser in the coming year and you find it a delightful experience!

--

--