Speed, Performance, and Public Profile

Co-authors: Gaurav Phapale and Oliver Tse

A public profile page is a guest-accessible LinkedIn member’s profile page. When you visit the same profile as logged-in user, it’s called a member profile. For example, when you visit Jeff’s profile, you see member profile when you are logged in to LinkedIn and you see public profile when you are logged out of LinkedIn. While both these pages show profile information of the same LinkedIn member, they have slightly different content and are optimized for different users.

A member profile is a personalized profile page experience for a LinkedIn member. For example, when I’m looking at Jeff’s profile, the member profile page is personalized for me, showing a “Relationship” module, “How you’re connected” module, etc. Apart from these personalized modules, other modules like “Skills” and “Recommendations” show detailed views of Jeff’s professional identity at LinkedIn.

A public profile on the other hand is optimized for guests (LinkedIn users who have not signed up yet) and search engine crawlers. A public profile page is a preview of the member profile page, without any personalization; each user sees the same public profile. It’s optimized for guests to get enough profile information to get them interested in LinkedIn and to induce them to sign up. It’s also optimized for search engine bots so they can crawl and index public profile pages to help these pages rank higher in search engine results.

Apart from the different user audience, member profile pages and public profile pages also differ in the way they are rendered. Member profile page is client-side rendered and has number of interactive elements. A public profile page on the other hand is server-side rendered, both because there are no interactive elements on the page and because search engine bots do not execute JavaScript.

This is Jeff Weiner’s public profile page:

Jeff Weiner

Page load time is a critical aspect of user experience. Higher page load time means more users leaving the website without actually seeing the page, resulting in fewer users signing up on website. Page speed is also an important factor for search engine ranking. Google uses page speed in their page rank algorithms. It is a key determinant for Search Engine Optimization (SEO). In addition, a slow page—high page load time—means that there are fewer pages indexed by search engine crawlers on a daily basis. In other words, it takes longer for an update on the public profile to improve a page’s SEO.

Until Q2 2015, the common service served both member and public profile pages with all optimizations targeted for both pages.

Given the different user focus with different optimizations of member and public profile pages, it was clear that we needed different services to host these pages. Public profile pages needed to focus on SEO optimizations, have the ability to alter and add new features, measure their SEO impact, and iterate quickly. The legacy profile service, which hosted both the member and public pages, was based on a legacy internal custom web framework. As a result, we had slower iterations. So, we decided to have a separate front end server to host the public profile page.

Given the importance of page speed for both guest users and search engine crawlers, when we started the rewrite of the public profile, page speed was our highest priority. We started with Play, an open-source web framework along with dust.js, an open-source HTML template framework.

There are a number of smaller modules that comprise a public profile page. The data for each module comes from downstream services via a REST API. For example:

  1. Main module (showing Summary, Experience, Education, Skills, etc.) data comes from the “identity” service.
  2. Connection count data comes from the connections service.
  3. Recommendations modules (showing recommendations received by a member) data comes from recommendations service.
  4. Groups module (showing the Groups a member belongs to) data comes from groups service.
  5. Posts module (showing quick-view of Posts written by member on LinkedIn publishing platform) data comes from the publishing service.
  6. Same-name search module (titled “Find a different Jeff Weiner” showing other members with the same name) data comes from the search service.
  7. Browsemap module (titled “People also viewed” showing top profile people visited after visiting current one) data comes from the browsemap service.

Apart from these modules, we needed to figure out what the member has chosen to show or hide. So the page also needed access to the “privacy settings” service.

These different service calls also need different data inputs. To gather data, some would need the member’s ID, while others would need the output from other downstream services.

In the Java Promise chain, we made sure that downstream calls were made as soon as possible. They must be resolved quickly so that they do not block public profile rendering.

For example, instead of waiting for the response from the “privacy settings” service to read the member’s settings for groups, posts, and recommendations and then decide to make corresponding service calls, we made requests asynchronously.

We made downstream service calls to groups, posts, and recommendations while making calls to the privacy settings service. Based on the privacy settings response, we decided to use or ignore responses from the downstream services.

For each new module or additional service call, we moved the call to the service as far left as possible in the Java promise chain timeline. With this thoughtful downstream call structure, we reduced server response time by 70 percent.

The default Play configurations that all LinkedIn services start with is good enough for a simple service that handles moderate load. The public profile frontend handles a lot of requests per second. The default Play configurations (specifically the heap memory allocation) caused the service to spend good amount of time in GC. We worked with SREs to optimize GC configuration for the public profile frontend service hosts, so that it optimizes for the behavior and load of the service giving us faster page speeds.

Public profile is server-side rendered (SSR), which means that the browser can render the page without JavaScript. That is, if we disable JavaScript on the browser, the primary or main content still appears. The primary or main content is the visual purpose of the page. For example, the purpose of a public profile is to provide the public a searchable preview of a LinkedIn member profile.

A page that renders on the server has certain advantages. One of them is a fast and near-complete first paint. First paint is when the browser displays initial content. In the case of a public profile, most of the content displays at or near this marker. This is ideal.

The focus of SSR is render first, JavaScript second. Unlike Client-Side Rendering (CSR), SSR is not CPU-bound. In other words, SSR does not require JavaScript for rendering. SSR is network-bound; optimizations made to improve SSR are of the traditional variety such as reducing overall page weight and requests.  

Note that SSR does not avoid running JavaScript.  It means deferring JavaScript so that it does not block rendering.

Reducing page weight begins with reducing HTML size. The results are fewer DOM and nested nodes. The more DOM nodes that we have, the heavier the page and the longer it takes for JavaScript to access the DOM.

In public profile, we reduced DOM nodes by 30 percent (1,959 to 1,400 nodes). We also reduced the nesting depth by 25 percent (12 to 9 nodes).  Overall, page weight improved 31 percent and coupled with downstream call improvements, page load improved 56 percent.

In addition, one of the first decisions we made was to avoid using JavaScript libraries. In the old public profile, we were using jQuery only for a small subset of its features such as finding elements, adding classes, async calls and event bindings. Since most browsers have good native support for such things, we decided not to use jQuery. Instead, we used native JavaScript. This reduced the overall weight of the page, the number of requested assets, and the time required to parse and execute extra libraries.

For external JavaScript, we used the “async” attribute on the HTML <script> tag. When the browser encounters an async attribute while parsing the HTML, the browser downloads the script in parallel. Async guarantees that downloading the script will not block HTML parsing. The browser evaluates the script when it is fully downloaded. In addition, we inlined scripts critical to page rendering. These scripts were extremely small and eliminated the need to have an additional request. We also deferred other assets that were not essential to viewing a public profile. We deferred loading of all images and ads.

Deferring loading images improved page load and ensured that the main or primary content rendered first. Combined with image transitioning (fading images in), deferring images reduced page jank (unwanted movement of visual elements as the browser renders the page), which provided a gradual and more aesthetically delightful experience.  

code

Defer loading ads on the other hand, prevented JavaScript from render blocking, so content rendering occurred more quickly.

code

Consider performance implications with advertisements, in particular. Ads are page ecosystems. They include their own HTML, CSS, JavaScript, images, etc. In the case of public profile, we deferred—after page load—almost 55 percent of those assets. For example, in one ad alone, we loaded three documents, one JavaScript and six images. It added 49K to overall page weight. This did not include the time the browser took to execute JavaScript or handle page layout and painting. More significantly, if not deferred, the ad would have contributed 18 percent of the time for public profile to reach visual completeness.

At LinkedIn, page load is the fundamental metric that we measure for performance. At page load, server-side rendered (SSR) pages are visually complete. To improve performance for public profile, we focused on rendering. We used time-tested techniques for reducing network latencies. This included improving back end response times, reducing overall page and asset weight (e.g. reducing the number of DOM nodes, avoiding the use of JavaScript libraries, defer loading JavaScript, images, etc.) and reducing the number of requests. These techniques focused on “network-bound” issues.

An important objective was to get to a fast and complete meaningful first paint. Ads were deferred. In other words, we loaded the ads after the page load event. Deferring ads removed any opportunity to block the critical path to rendering. Deferring ads or deferring JavaScript execution focused on reducing “CPU-bound” issues.

Very seldom is performance limited to the front end or back end. It requires teamwork to achieve the most optimal performance.

Public profile frontend work was an amazing team effort and we’d like to thank Shane Afsar, Anand Kishore and Brendan Speth for their constant support during design, implementation and ramping of the project. It would not have been possible to achieve the performance improvements without the immense help from Cliff Snyder. We would also like to thank Ajit Datar who has been constantly a source of encouragement and support, and last but not the least, Ben Lai for all his help reviewing and editing this blog post.