Ashley Watkins discussed at React Conf some of the technologies and strategies powering FB5, the new facebook.com, addressing topics such as data-driven dependencies, phased code and data downloading, and more.
The new facebook.com website is a single-page web application built on a React/GraphQL/Relay stack. GraphQL is a query language with which developers may specify the pieces of data that they need. Relay is a framework that integrates with React for constructing GraphQL queries and handles data fetching. Watkins explained that by standardizing Facebook’s tech stack around the previous three technologies, Facebook was able to rethink how to operate at scale while optimizing for user experience.
A key challenge of single-page applications is the minimization of the time-to-visually-complete, i.e. the amount of time it takes between when a user navigates to a website and when the above-the-fold visible content is rendered. In a standard implementation of a single-page app, the client requests a page, the server sends the HTML and JavaScript for the page, which triggers the fetching of some data. While data is being fetched, a loading screen is displayed.
With the new facebook.com architecture, the HTML document requested by the browser is streamed down to the client. As that happens, the browser incrementally parses the document and starts to download the scripts referred to in the HTML file. Facebook servers write the HTML page so that it starts with the CSS and JavaScript resources that all users are going to need, and continues with page- and user-specific resources which are slower to compute. At that point, the server can start fetching data as indicated by Relay. The HTML page ends with a script tag including the data prefetched by Relay, when that data becomes available. The end result is that JavaScript and data are downloaded in parallel. This optimized process is thus rendered possible by HTML flushing and the centralized description with GraphQL of the data required by the page. In the best cases, the time savings may eliminate the need to display a loading screen.
Facebook also optimizes for time-to-first-paint, which is the time necessary to display the first pixels on the screen (thus necessarily shorter than the time-to-visually-complete). To that purpose, Facebook uses phased code-splitting. With phased code-splitting, code is split across three buckets of increasing priority and downloaded in three phases. The first bucket relates to the loading page, the second bucket contains the necessary information that impacts visually the page while the third bucket gathers the code and data that are orthogonal to displaying concerns (like analytics). Facebook added two APIs, importForDisplay()
(phase 2) and importForAfterDisplay()
(phase 3) to assign components to phases. At the end of the each of the first two phases, a render occurs. The first render thus occurs before the full code for the page is fetched, shortening the time-to-first-paint. Because phase 3 only incorporates information that does not affect the screen, the screen is complete by the end of Phase 2 and the time-to-visually-complete is also shortened.
Facebook additionally optimizes to get the primary content as early as possible, by minimizing time-to-meaningful-paint. While the previous optimization strategy involves smart code-splitting, the time-to-meaningful-paint optimization strategy involves data splitting. In this strategy, critical data must arrive first and be used immediately. It is for instance rarely necessary to show more than one newsfeed post for the initial render of the page. To support data splitting, Relay introduced streamed queries in which queries parts which can be streamed down are annotated with the @stream
marker:
fragment HomepageData on User {
newsFeed(first: 10) (
edges @stream
}
-- Additional data @defer
}
The @defer annotation also allows indicating which parts of the queried data are not critical. The meaningful paint thus comes earlier, and as additional data is received, the view is hydrated and the screen updated. Additionally, the time-to-visually-complete may also be lowered, as the critical code and data delivered first typically relate to above-the-fold content.
The last optimization strategy consists of not fetching code that will not be used. Watkins gave the example of two variations of the same component, as may occur for A/B testing purposes. The second component version offers distinct features, and comes with additional code which is only necessary when the user is selected in the A/B group. Watkins observed that a first idea – implementing the feature toggle with dynamic imports, does not marry well with streamed renders. Dynamic imports result from the execution on the client of previously downloaded code. This means there are gaps between consecutive dynamic imports while code is parsed and executed, which further delays rendering. Facebook implements feature toggles so that they can be detected when the page request is received by the server which then ships only the right code.
Another case is that of components with several variants, one of which is selected dynamically depending on the fetched data. For example, a Post component may delegate to a VideoPost or PhotoPost component depending on the fetched post type or content. Each of these components may have its own data requirements. A standard implementation thus leads to download the code and data for all variants. In this case, facebook.com applies an optimization strategy dubbed data-driven code-splitting, which relies on the GraphQL description of the variant components’ queries and @module
Relay annotations:
... on Post {
... on PhotoPost {
@module('PhotoComponent.js')
photo_data
}
... on VideoPost {
@module('VideoComponent.js')
video_data
}
... on SongPost {
@module('SongComponent.js')
song_data
}
}
As code and data are streamed in parallel, the page is progressively rendered, and component rendering needs to be coordinated to avoid having content moving around as data and images arrive at different times. The facebook.com website developers leverage React Suspense for rendering coordination purposes, optimizing the perceived user loading experience.
Watkins’s full talk is available on ReactConf’s site and contains further code snippets and detailed explanations. React Conf is the official Facebook React event. React Conf was held in 2019 in Henderson, Nevada, on October 24 & 25.