Implementing Instant Job Listing Pages
April 20, 2017
In early February, we reduced our 90th percentile U.S. subsequent page load time by 46% on our Job Details page. We achieved this by using the Ember cache to display critical data and avoid making an API call to fetch the initial data we render.
There are a few strategies you could use for passing cached data between pages in a Single Page App (SPA). In this post, we’ll describe an API pattern our team calls “Frontend Deco,” which allows a client to quickly change the field projection used when fetching job models throughout the site. We will also explain how Frontend Deco allowed our team to set up the site speed experiments which lead to a sub-second page load time.
Let’s build some context about the Jobs ecosystem
The Job Details page gets a lot of subsequent traffic from Jobs pages like Jobs Search, Jobs Home, and other Job Details pages. We define a subsequent page view as being when a member clicks on a job posting somewhere on LinkedIn, causing the SPA to change to the Job Details page in the same tab.
We used an internal LinkedIn tool that aggregates page tracking information to determine navigation trends in order to better understand how to optimize subsequent page loads. Looking at the following chart, it is easy to see that the majority of Job Details traffic comes from three sources (in order): Jobs Search, other Job Details pages, and Jobs Home.
Jobs Details traffic sources
Frontend Deco allows clients to specify a field projection when fetching a model. For example, on our Job Details page, we fetch a highly-decorated jobPosting model with fields like “numberOfViews” and “description.” On other properties, like Jobs Home or Jobs Search, we fetch a subset of the fields we display on Job Details to render job cards (we only need to display a few things like “title” and “companyName”). We use Frontend Deco to specify the fields each time we call the jobPosting API. The API returns the same job model, just with a subset of all fields available.
Our job model might look something like this:
When fetching the job with Frontend Deco, we specify a projection on the model using a special decoration query parameter. The API call might look like this:
Even though the backing model is much larger, the data returned looks like this:
In this example, we only fetched the two fields we needed (title, companyName). This offers two performance advantages:
The numberOfViews field is very expensive to fetch (~50ms) when compared to the title and companyName field (~5ms). Our API is built to understand the field projection before it starts to build the jobPosting model, so we don’t make the downstream call required to calculate numberOfViews if it is not present in the projection.
The description field is often longer than a hundred words, which means that omitting it requires less data transfer time.
When we display job cards leading to the Job Details page, the projection is more complicated than in the example below, but the idea is the same:
Frontend Deco allows us to have one job model throughout the website, but only pay the performance penalties for the fields we need when we display the job on a different page. Additionally, having just one job model makes it really easy to use cached data when rendering the Job Details page. Imagine if you had multiple view models, each with the information to display a job: you would need to code the Job Details template to render each different model and you would need to look up each variation of the job model in the Ember store when looking for cached data!
Fetching cached data in Ember
The code for data pass through on the Job Details route works like this:
Getting access to the data we have on the previous page is simple. However, not all the data is present that we might require for first render. The downside to simply showing the data available in the cache on first render is that you might display a poor experience for the user.
We consider Job Details to be rendered when “top card” is fully loaded. The top card includes the job description, a field on the jobPosting model that is not required to display the job cards in Jobs Search and Jobs Home. The screenshot below shows the data we need available from the cache when rendering the Job Details page.
Top card of Job Details page using cached data
Site speed experiments to ensure job description is present
The job description is not displayed on job cards but we want that data available in the cache before we hit the Job Details route. We did two experiments in order to determine the best method for having the description available. First, we simply added the job description to the field projection used when fetching the job models for the job cards on Jobs Home. This means that the API call on Jobs Home that fetches the job cards actually fetched the job description as well, even though it is not displayed on that page. When we ramped this experiment, we saw that we were able to render the top card on the Job Details page with the job description present. However, we also saw a performance hit on the Jobs Home page.
In the second experiment, we didn’t change the field projection used to fetch the job cards (we only fetched the fields needed to render the cards) and we instead performed a second API call to fetch just the job description (more specifically, we used Frontend Deco to fetch jobPosting models with just the description field present). Our concern with this approach was that a member might click on a job card before the description is available. We have code in the Job Details route to block render until the description is present (so our numbers are accurate).
Our data shows that by the time a user clicks on the job posting card, the description has been merged into the job posting model used to display the job cards, and is present when we render the Job Details page without blocking the Job Details render with an API call. We know this because we see our site speed improve, even though we have code that ensures the description is present.
We decided to ramp the second experiment to all members and implemented it on other routes that lead to Job Details (before, we were experimenting with just Jobs Home). With this change, we saw a 46% site speed improvement in the 90th percentile, reducing Job Details subsequent page load time to under 1 second, with no impact to the Jobs pages that prefetch the job description.
Using cached data versus making an API call (90th percentile, subsequent page load time)
Before and after comparison
We see a big improvement in our metrics, but it also noticeably feels like the page now changes instantly as well. This video shows a slow motion comparison with cached data (top) versus the old implementation (bottom).
There are a few ways to approach data pass through, but we believe a projection-based approach is best. A projection-based approach allows us to quickly change which fields we fetch for a given model.
Measure all performance changes. Measuring our work made it easy to determine the best way to prefetch the job description.
Understand your traffic sources. Looking at the pages most commonly leading to Job Details helped us target our efforts to optimize for the common case.
Make sure the page feels faster. Even if you drive a site speed metric down, make sure the perceived performance of the page is good too.
This work would not have been possible without the help of a few peers. Paul Yuan and Bradley Walker helped implement this change on the Job Details page. Additionally, Paul created the side-by-side comparison video. Joe Florencio helped guide my team towards the Frontend Deco pattern.
This would also not have been possible without the support of the Foundation Team at LinkedIn, who created Frontend Deco.