Facebook detailed in a blog post the technologies and strategies powering FB5, the latest iteration of the facebook.com website. Facebook rearchitected its website and standardized its technological stack around React, GraphQL, Relay, and its custom CSS-in-JS library. The goal of the rewrite was to increase performance and make it easy to add new features.
Facebook explained the motivation behind FB5 as follows:
When we thought about how we would build a new web app, […] we realized that our existing tech stack wasn’t able to support the app-like feel and performance we needed. A complete rewrite is extremely rare, but in this case […] we knew it was the only way we’d be able to achieve our goals for performance and sustainable future growth. […]
We knew we wanted Facebook.com to start up fast, respond fast, and provide a highly interactive experience.
Facebook started in 2004 as a server-driven PHP website. For FB5, Facebook chose to build a client-driven app, which better supports highly interactive experiences for users. This meant addressing performance and responsiveness issues in a way that is resilient to the regular addition of new features. The techniques put in place addressed four key constituents of a web application: styling (CSS), interactivity (JavaScript), remote data fetching, and navigation.
Facebook’s Frank Yan reported reducing CSS on the homepage by 80 percent, from 413 KB to 73 KB. A custom CSS-in-JS library helped remove unused styles and reuse CSS rules. Colocating styles with the component code that uses it allows the component and its styles to change—and disappear together, reflecting their coupling. Atomic CSS, an approach to CSS architecture that favors small single-purpose CSS classes, enabled deduplication and reuse of CSS rules. Yan credited atomic CSS with breaking the linear growth pattern observed between CSS file size and features. A custom build tool splits the colocated styles into separate, optimized bundles.
Facebook furthermore improved theming by leveraging custom CSS properties; increased accessibility by switching from a px
basis to a font-size-proportional rem
basis; bettered user experience by inlining SVGs into HTML rather than passing SVG files to <img>
tags. The latter prevented flickering as icons may come in after the rest of the content.
Implementing the interactive features of a web application often results in a JavaScript codebase that is growing with the number and complexity of the application features. With the amount of code and data downloaded being a primary driver of page load speed and thus user experience, Facebook used a mix of code-splitting and optimized data fetching strategies to fetch only what is needed as early as possible.
The application code is split into three tiers that are downloaded separately and signaled with custom import APIs. The first tier gathers the basic layout needed to display the first paint for the above-the-fold content. That includes UI skeletons for initial loading states. Code in the first tier uses a regular import
syntax to import modules.
The second tier includes all the JavaScript needed to fully render all above-the-fold content. Code in the second tier imports dependencies with a custom importForDisplay
API. The blog post explained:
Once an
importForDisplay
is encountered, it and its dependencies are moved into Tier 2. This returns a promise-based wrapper to access the module once it’s loaded.
[…] Tier 2 needs to be fully interactive. If someone clicks on a menu after Tier 2 code loads and renders, they get immediate feedback about the interaction, even if the contents of the menu are not ready to render.
The third tier gets any code that is left, that is, code that does not participate in the rendering of above-the-fold content. The third tier also uses a custom importForAfterDisplay
API to declare its dependencies.
Users can thus view something on the screen faster (layout and placeholders rendered by Tier 1 code). They can also typically enjoy an interactive application before code in the third tier is downloaded and executed.
The previously described code-splitting strategy is complemented by dynamically and selectively including only the necessary code in two specific cases.
A/B testing is supported with an importCond
API that declares the A/B dependency logic. The server may then send down only the required version of the component instead of both versions together. This strategy can be used for UIs that have several variations, only one of which is used.
Components may branch into one of several sub-components depending on fetched data (e.g., FeedPost
, PhotoPost
). Only the sub-component that will actually be branched should be imported. Ashley Watkins, software engineer at Facebook, called the corresponding strategy data-driven code-splitting. In a talk at React conf, Watkins described in detail how HTML flushing, the centralized description with GraphQL of the data required by a page, and dedicated Relay APIs participated in streaming down to the client just the code and data, as annotated by the developer. An example of annotations of a Relay query is as follows:
... on Post {
... on PhotoPost {
@module('PhotoComponent.js')
photo_data
}
... on VideoPost {
@module('VideoComponent.js')
video_data
}
... on SongPost {
@module('SongComponent.js')
song_data
}
}
The colocation of queries and their code dependencies creates a coupling that is used to selectively include only the relevant code.
In addition to optimizing at compile time and run time the size of code downloaded to the user, Facebook also optimized data-fetching. Facebook achieved gains by standardizing its data fetching stack (GraphQL), preloading data on the initial server request to improve startup time, and streaming priority data ahead of non-priority data.
Joe Savona detailed at React Conf how GraphQL, Relay, and Suspense cooperate to deliver optimized data fetching. Relay knows at build time what data the page needs. This enables starting to fetch the required data as soon as a request for a page is received, in parallel with the required code. The achieved parallelism eschews extra round trips and renders the final page content sooner.
Facebook developers annotate GraphQL queries with internal GraphQL extensions (@stream, @defer) to declaratively segregate priority data (that is immediately streamed when ready) from non-priority data (that is not needed right away).
Fast navigation is an important feature of single-page applications. Facebook uses a mix of dynamic routing, prefetching, and parallelization strategies to achieve seamless transitions between pages.
To transition to another part of the application, the application needs to know the application routes (route map), and the code and data for the route to transition to. As Facebook’s route map is too large to send all at once, routes are dynamically added, as new links are rendered.
Route resources are fetched before the browser actually navigates to a route. On desktops, code may be prefetched when the user hovers on a link (resource hint rel=preload
), code (script tag) and data are loaded on the mousedown event, and rendered with Suspense on click. React Suspense transitions ensure that the user navigates directly into a fully-formed page (whether a skeleton page or the fully rendered page), without showing an intermediate blank screen.
To fetch code and data in parallel instead of sequentially, Facebook uses an EntryPoints
API that captures the query fetching the necessary data for the route.
Developers interested in an in-depth presentation of the technologies behind FB5 may refer to the Facebook blog post covering the redesign effort. Additionally, React Conf talks covering CSS, data fetching, and other optimization strategies are available online, and provide further details, illustrations and demos.
GraphQL is a query and manipulation language for APIs and a runtime. GraphQL was developed internally by Facebook in 2012 before being publicly released in 2015. GraphQL supports reading, writing, and subscribing to changes to data. GraphQL helps developers decouple API providers from consumers by describing what data the consumer needs instead of how data is to be fetched.
Relay is a GraphQL client for React. Relay relies on React components locally declaring their data dependencies. Relay minimizes roundtrips by fetching the data dependencies for all the components in a single GraphQL request. Relay additionally provides APIs that support optimistic updates and synchronize components with their data dependencies.