Loading... > 原文FaceBook地址:<span class="external-link"><a href="https://engineering.fb.com/web/facebook-redesign/" target="_blank">https://engineering.fb.com/web/facebook-redesign/<i data-feather='external-link'></i></a></span> Facebook.com launched in 2004 as a simple, server-rendered PHP website. Over time, we’ve added layer upon layer of new technology to deliver more interactive features. Each of these new features and technologies incrementally slowed the site down and made it harder to maintain. This made it harder to introduce new experiences. Features like dark mode and saving your place in News Feed had no straightforward technical implementation. We needed to take a step back to rethink our architecture. When we thought about how we would build a new web app — one designed for today’s browsers, with the features people expect from Facebook — 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, since so much has changed on the web over the course of the past decade, we knew it was the only way we’d be able to achieve our goals for performance and sustainable future growth. Today, we’re sharing the lessons we’ve learned while rearchitecting Facebook.com, using <span class="external-link"><a href="https://reactjs.org/" target="_blank">React<i data-feather='external-link'></i></a></span> (a declarative JavaScript library for building user interfaces) and <span class="external-link"><a href="https://relay.dev/" target="_blank">Relay<i data-feather='external-link'></i></a></span> (a GraphQL client for React). > 当我们考虑如何构建一个新的网络应用—一个为现代浏览器设计的、具有用户对Facebook(我们已知的)所有期望的功能,我们现有的技术栈无法支持我们所需要的类似于桌面应用的感觉和性能。完全重写是非常罕见的,但在这种情况下,由于过去十年来Web技术发生了很多变化,我们知道这是我们实现性能和未来可持续发展目标的唯一途径。今天,我们就分享一下我们在重构Facebook.com时的经验教训,使用React(一种用于构建用户界面的声明式JavaScript库)和Relay(React的GraphQL客户端)来重构Facebook.com。 ## Getting started > 开始 We knew we wanted Facebook.com to start up fast, respond fast, and provide a highly interactive experience. Although a server-driven app could deliver a fast startup time, we weren’t convinced we could make it as interactive and delightful as a client-driven app. However, we believed we could build a client-driven app with a competitively fast startup time. But starting from the ground up with a client-first app brought a new set of problems. We needed to rebuild the tech stack quickly while also addressing speed and other user experience issues — and we needed to do it in such a way that it would be sustainable for years to come. Throughout the process, we anchored our work around two technical mantras: 1. **As little as possible, as early as possible.** We should deliver only the resources we need, and we should strive to have them arrive right before we need them. 2. **Engineering experience in service of user experience.** The end goal of our development is all about the people using our website. As we think about the UX challenges on our site, we can adapt the experience to guide engineers to do the right thing by default. We applied these same principles to improve four main elements of the site: CSS, JavaScript, data, and navigation. > 我们希望Facebook.com能够快速启动,快速响应,并提供高度互动的体验。虽然服务端驱动(server-driven)的应用程序可以提供快速启动时间,但我们不相信它能像客户端驱动(client-driven)的应用程序那样具有互动性和愉悦性。然而,我们相信我们可以构建一个客户端驱动的应用程序,并能提供具有竞争力的快速启动时间。 > > 但是从头开始做一个客户端优先的APP,这带来了一系列新的问题。我们需要快速重建网站,同时解决速度和其他用户体验问题,而且在未来几年内能可持续的发展。在整个过程中,我们围绕着两个技术口号开展工作: > > 1. **尽可能少,尽可能早**。只提供所需要的资源,而且能在需要的时候及时送达。 > 2. **服务于用户体验的工程体验**。我们开发的最终目标是为了我们的用户。当思考用户体验的挑战时,我们需要引导工程师默认做正确的事情来适配体验需求。我们应用这些原则来改进网站的四个要素:**CSS、JavaScript、数据和路由**。 ## Rethinking CSS to unlock new capabilities > 反思CSS,解锁新功能 First, we reduced the CSS on the homepage by 80 percent by changing how we write and build our styles. On the new site, the CSS we write is different from what gets sent to the browser. While we write familiar CSS-like JavaScript in the same files as our components, a build tool splits these styles into separate, optimized bundles. As a result, the new site ships less CSS, supports dark mode and dynamic font sizes for accessibility, and has improved image rendering performance — all while making it easier for engineers to work with. > 首先,我们通过改变编写和构建样式的方式,将主页上的CSS减少了80%。在新网站上,我们写的CSS与在浏览器上看到的CSS不同。当我们将CSS-like的JavaScript和组件写在一起时,构建工具会将这些样式分割成单独的优化包。因此,新网站的CSS数量减少了,支持暗模式和动态字体大小以实现可访问性,并改善了图片的渲染性能,同时让工程师们开发更容易。 ### Generating atomic CSS to reduce homepage CSS by 80 percent > 原子化的CSS,减少主页80%的CSS On our old site, we were loading more than 400 KB of compressed CSS (2 MB uncompressed) when loading the homepage, but only 10 percent of that was actually used for the initial render. We didn’t start out with that much CSS; it just grew over time and rarely decreased. This happened in part because every new feature meant adding new CSS. We addressed this by generating atomic CSS at build time. Atomic CSS has a logarithmic growth curve because it’s proportional to the number of unique style declarations rather than to the number of styles and features we write. This lets us combine the generated atomic CSS from across our site into a single, small, shared stylesheet. As a result, the new homepage downloads less than 20 percent of the CSS the old site downloaded. > 在我们的旧网站上加载主页时,加载了超过400KB的压缩CSS(2MB未压缩),但实际上只有10%的CSS被用于初始渲染。我们一开始并没有使用那么多的CSS,只是随着时间的推移而增加,很少做删减。之所以会出现这种情况,部分原因是每一个新功能都意味要添加新的CSS。 > > 我们通过在构建时生成原子化CSS来解决这个问题。原子化CSS有一个对数增长曲线,因为**它与唯一的样式声明的数量成正比,而不是与我们编写的样式和功能的数量成正比**。这使得我们可以将整个网站中生成的原子型CSS合并到一个单一的、小的、共享的样式中。结果是新主页CSS下载量不到老网站的20%。 ### Colocating styles to reduce unused CSS and make it easier to maintain > 协同定位样式(Colocating styles)减少未使用的CSS,使其更容易维护 Another reason our CSS grew over time was that it was difficult to identify whether various CSS rules were still in use. Atomic CSS helps mitigate the performance impact of this, but unique styles still add unnecessary bytes, and the unused CSS in our source code adds engineering overhead. Now, we colocate our styles with our components so they can be deleted in tandem, and only split them into separate bundles at build time. We also addressed another issue we were facing: CSS precedence depends on ordering, which is especially difficult to manage when using automated packaging that can change over time. It was previously possible for changes in one file to break the styles in another without the author realizing it. Instead, we now author styles using a familiar syntax inspired by <span class="external-link"><a href="https://reactnative.dev/" target="_blank">React Native<i data-feather='external-link'></i></a></span> styling APIs: We guarantee that the styles are applied in a stable order, and we don’t support CSS descendant selectors. > CSS随着时间的推移而增长的另一个原因是我们很难识别各种CSS规则是否还在使用。Atomic CSS有助于缓解这一点的性能影响,但独特的样式仍然会增加不必要的字节,而且我们的源代码中未使用的CSS会增加工程开销。现在,我们将我们的样式与我们的组件写在一起,这样就可以将它们串联起来删除,并且只在构建时将它们分割成单独的包。 > > 我们还解决了另一个问题,CSS的优先级取决于顺序,当使用自动打包时,这一点尤其难以管理,因为自动打包会随着时间的推移而改变。以前,一个文件中的变化可能会在作者没有意识到的情况下破坏另一个文件中的样式。相反,我们现在用一种熟悉的语法来编写样式,它的灵感来自于React Native风格的API。我们保证样式以稳定的顺序应用,而且不支持CSS后裔选择器。 ### Changing font sizes for better accessibility > 改变字体大小以提高无障碍性 We’ve taken advantage of our offline build step to make accessibility updates as well. On many websites today, people enlarge text by using their browser’s zoom function. This can accidentally trigger a tablet or mobile layout or increase the size of things they didn’t need to enlarge, such as images. By using <span class="external-link"><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/font-size" target="_blank">rems<i data-feather='external-link'></i></a></span>, we can respect user-specified defaults and are able to provide controls for customizing font size without requiring changes to the stylesheet. Designs, however, are usually created using CSS pixel values. Manually converting to rems adds engineering overhead and the potential for bugs, so we have our build tool do this conversion for us. > 在今天的许多网站上,人们会通过使用浏览器的缩放功能放大文字。这可能会不小心触发平板电脑或移动端布局,或者改变不需要放大的东西,比如图片。 > > 通过使用rems[3],我们可以遵守用户指定的默认值,并且能够提供对自定义字体大小的控制,而不需要修改CSS。然而,设计通常是使用CSS像素值创建的。手动转换为rems会增加工程开销和潜在的bug,所以我们的构建工具自动完成这个转换。 > > ## ### Sample build-time handling > 构建时处理的例子 ```javascript const styles = stylex.create({ emphasis: { fontWeight: 'bold', }, text: { fontSize: '16px', fontWeight: 'normal', }, }); function MyComponent(props) { return <span className={styles('text', props.isEmphasized && 'emphasis')} />; } ``` *Example of source code.* > 这是源代码 ```css .c0 { font-weight: bold; } .c1 { font-weight: normal; } .c2 { font-size: 0.9rem; } ``` *Example of generated CSS.* > 生成的CSS ```javascript function MyComponent(props) { return <span className={(props.isEmphasized ? 'c0 ' : 'c1 ') + 'c2 '} />; } ``` *Example of generated JavaScript.* > 生成的JavaScript ### CSS variables for theming (dark mode) > 用于主题设计的CSS变量(暗夜模式) On the old site, we used to attempt to apply themes by adding a class name to the body element and then using that class name to override existing styles with rules that had a higher specificity. This approach has issues, and it no longer works with our new atomic CSS-in-JavaScript approach, so we have switched to <span class="external-link"><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties" target="_blank">CSS variables<i data-feather='external-link'></i></a></span> for theming. CSS variables are defined under a class, and when that class is applied to a DOM element, its values are applied to the styles within its DOM subtree. This lets us combine the themes into a single stylesheet, meaning toggling different themes doesn’t require reloading the page, different pages can have different themes without downloading additional CSS, and different products can use different themes side by side on the same page. > 在旧网站上,我们曾经尝试通过在body元素中添加一个类名来应用主题,然后用这个类名来覆盖现有的样式,这些样式有更高的优先级。这种方法有问题,它不再适用于我们新的原子化的CSS-in-JavaScript方法,所以我们改用CSS变量来进行主题切换。 > > CSS变量被定义在一个类下,当这个类应用到DOM元素上时,它的值会被应用到它的DOM子树中的样式。这让我们可以将主题组合成一个单一的样式表,这意味着切换不同的主题不需要重新加载页面,不同的页面可以有不同的主题而不需要下载额外的CSS,不同的产品可以在同一个页面上并排使用不同的主题。 ```css .light-theme { --card-bg: #eee; } .dark-theme { --card-bg: #111; } .card { background-color: var(--card-bg); } ``` This made the performance impact of a theme proportional to the size of the color palette rather than to the size or complexity of the component library. A single atomic CSS bundle also includes the dark mode implementation. > 这使得主题的性能影响与调色板的大小成正比,而不是与组件库的大小或复杂性成比例。单个原子CSS包还包括暗模式实现。 ### SVGs in JavaScript for fast, single-render performance > 在JavaScript中使用SVG,实现快速、单一渲染的性能 To prevent flickering as icons come in after the rest of the content, we inline SVGs into the HTML using React rather than passing SVG files to <img> tags. Because these SVGs are now effectively JavaScript, they can be bundled and delivered together with their surrounding components for a clean one-pass render. We’ve found that the upside of loading these at the same time as the JavaScript was greater than the cost of SVG painting performance. By inlining, there’s no flickering of icons that pop in afterward. > 为了防止图标在其他内容之后出现闪烁,我们使用 React 将 SVG 内联到 HTML 中,而不是将 SVG 以img的方式显示。因为这些SVG现在是有效的JavaScript,所以它们可以和周围的组件一起实现干净的单次渲染。我们发现,在加载JavaScript的同时加载这些SVG的好处大于SVG的绘制性能。通过内联,不会出现图标闪烁。 ```javascript function MyIcon(props) { return ( <svg {...props} className={styles({/*...*/})}> <path d="M17.5 ... 25.479Z" /> </svg> ); } ``` Additionally, these icons can change colors smoothly at runtime without requiring further downloads. We’re able to style the icon according to its props and use our CSS variables to theme certain types of icons, especially ones that are monochrome. > 此外,这些图标可以在运行时平滑地改变颜色,而无需进一步下载。我们可以根据图标的道具设置图标的样式,并使用CSS变量为某些类型的图标设置主题,特别是那些单色的图标。 <img src="https://engineering.fb.com/wp-content/uploads/2020/05/1.-Home-Setting-Light-Mode.png" alt="New Facebook.com design in light mode: Rebuilding our tech stack for the new Facebook.com " /> <img src="https://engineering.fb.com/wp-content/uploads/2020/05/2.-Home-Setting-Dark-Mode.png" alt="Facebook.com redesign in dark mode" /> ## Code-splitting JavaScript for faster performance > JavaScript通过Code-splitting提高性能 Code size is one of the biggest concerns with a JavaScript-based single-page app because it has a large influence on page load performance. We knew that if we wanted a client-side React app for Facebook.com, we’d need to solve for this. We introduced several new APIs that work in line with our “as little as possible, as early as possible” mantra. > 代码大小是一个基于JavaScript的单页面应用最大的担忧之一,因为它对页面加载性能影响很大。我们知道,如果我们想让Facebook.com的客户端React app有客户端的效果,就需要解决这个问题。我们引入了几个新的API,这些API的工作原理与我们 "尽可能少,尽可能早"的口号一致。 ### Incremental code download to deliver just what we need, when we need it > 递增的代码加载,在需要的时候提供需要的东西(what we need, when we need it) When someone is waiting for a page to load, our goal is to give immediate feedback by rendering UI “skeletons” of what the page is going to look like. This skeleton needs minimal resources, but we can’t render it early if our code is packaged in a single bundle, so we need to code-split into bundles based on the order in which the page should be displayed. However, if we do this naively (i.e., by using <span class="external-link"><a href="https://github.com/tc39/proposal-dynamic-import" target="_blank">dynamic imports<i data-feather='external-link'></i></a></span> that are fetched during render), we could hurt performance instead of helping it. This is the basis of our code-splitting design of JavaScript Loading Tiers: We split the JavaScript needed for the initial load into three tiers, using a declarative, statically analyzable API. Tier 1 is the basic layout needed to display the first paint for the above-the-fold content, including UI skeletons for initial loading states. > 在等待页面加载的时候,我们的目标是通过渲染页面的UI "骨架 "来即时反馈页面会是什么样子。这个骨架需要最少的资源,但如果代码被打成一个包,我们就无法提前渲染,所以我们需要根据页面显示的顺序将代码拆分成包。然而,如果简单地这样干(即使用在渲染过程中获取的动态导入),我们可能会伤害到性能,而不是有利于性能。这就是我们对“**JavaScript加载层**”的代码拆分设计的基础。**我们将初始加载所需的JavaScript分成三层,使用一个声明式的、可静态分析的API**。 > > **第1层**是显示上层内容的首刷所需的基本布局,包括初始加载状态的UI骨架。 <img src="https://engineering.fb.com/wp-content/uploads/2020/05/3.-Tier-1.png" alt="Tier 1 is the basic layout needed to display the first paint for the above-the-fold content, including UI skeletons for initial loading states." />The page after Tier 1 code loads and renders. > 第一层代码加载和渲染后的页面 ```javascript import ModuleA from 'ModuleA'; ``` *Tier 1 uses regular `import`syntax.* Tier 2 includes all the JavaScript needed to fully render all above-the-fold content. After Tier 2, nothing on the screen should still be visually changing as a result of code loading. > 第1层使用常规的导入方式 > > **第2层**包括了所有需要的JavaScript,以完全呈现所有的折叠内容。第2层之后,屏幕上的任何内容都不应该因为代码加载而发生视觉上的变化。 <img src="https://engineering.fb.com/wp-content/uploads/2020/05/4.-Tier-2.png" alt="Tier 2 includes all the JavaScript needed to fully render all above-the-fold content. After Tier 2, nothing on the screen should still be visually changing as a result of code loading." />The page after Tier 2 code loads and renders. > 第2层代码加载和渲染后的页面 ```javascript importForDisplay ModuleBDeferred from 'ModuleB'; ``` *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.* > 一旦遇到一个importForDisplay,它和它的依赖关系就会被移到第2层。返回一个基于promise包装的模块,以便在模块加载后访问它 <img src="https://engineering.fb.com/wp-content/uploads/2020/05/5.-Tier-2-after-Interaction.png" alt="Tier 3 includes everything that is only needed after display that doesn’t affect the current pixels on the screen, including logging code and subscriptions for live-updating data." />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. Tier 3 includes everything that is only needed after display that doesn’t affect the current pixels on the screen, including logging code and subscriptions for live-updating data. > 第2层需要完整的交互。如果有人在第2层代码加载和渲染后点击菜单,即使菜单的内容还没有准备好渲染,也会立即得到反馈。) > > **第3层**包含显示后才需要的、不影响当前屏幕展示的所有东西,包括log代码和订阅实时更新数据的代码。 ```javascript importForAfterDisplay ModuleCDeferred from 'ModuleC'; // ... function onClick(e) { ModuleCDeferred.onReady(ModuleC => { ModuleC.log('Click happened! ', e); }); } ``` *Once an `importForAfterDisplay` is encountered, it and its dependencies are moved into Tier 3. This returns a promise-based wrapper to access the module once it’s loaded.* A 500 KB JavaScript page can become 50 KB in Tier 1, 150 KB in Tier 2, and 300 KB in Tier 3. Splitting our code this way enables us to improve time to first paint and time to visual completion by reducing the amount of code that needs to be downloaded to hit each milestone. Because Tier 3 doesn’t affect the pixels on the screen, it isn’t really a render, and the final paint finishes earlier. Most significantly, the loading screen is able to render much earlier. > 一旦遇到importForAfterDisplay,它和它的依赖关系就会被移到第3层。返回一个基于promise包装的模块,以便在模块加载后访问它。 > 一个500KB的JavaScript页面,在第1层可以变成50KB,第2层可以变成150KB,第3层可以变成300KB。以这种方式分割代码,使我们能够通过减少需要下载的代码量来达到每一个里程碑,从而提高了从第一次绘制到视觉完成的时间。因为第3层并不影响屏幕上的像素,所以它并不是真正的渲染,最终的刷图完成时间更早。最重要的是,加载屏幕能够更早地渲染。 ### Delivering experiment-driven dependencies only when they’re needed > 只有在需要的时候才加载的试验驱动(experiment-driven)的依赖项 We often need to render two variations of the same UI, e.g., in an A/B test. The simplest way to do this is to download both versions for all people, but this means we often download code that is never executed. A slightly better approach is to use dynamic imports on render, but this can be slow. Instead, in keeping with our “as little as possible, as early as possible” mantra, we built a declarative API that alerts us to these decisions early and encodes them in our dependency graph. As the page is loading, the server is able to check the experiment and send down only the required version of the code. > 我们经常需要渲染两个相同的UI的变体,例如在A/B测试中经常需要渲染两个相同的UI。最简单的方法是下载两个版本,但这意味着下载的代码可能永远不会被执行。一个稍微好一点的方法是在渲染时动态导入,但这可能会很慢。 > > 相反,为了保持我们的 "尽可能少,尽可能早 "的口号,我们构建了一个声明式的API,可以提前提醒我们这些决定,并将其编码到我们的依赖图中。当页面正在加载时,服务器能够检查试验,并只向下发送所需版本的代码。 ```javascript const Composer = importCond('NewComposerExperiment', { true: 'NewComposer', false: 'OldComposer', }); ``` This works well when the conditions we split on are static across page loads for that person, such as A/B tests, locales, or device classes. > 当我们分割的条件是静态的跨页面加载时,例如A/B测试、区域设置或设备类,这种方法非常有效。 ### Delivering data-driven dependencies only when they’re needed > 仅在需要时才加载的数据驱动(data-driven)的依赖项 What about code branches that are not static across page loads? For example, sending down all the rendering code for all the different types and combinations of components for News Feed posts would considerably bloat the page’s JavaScript size. These dependencies are decided at runtime, based on which data is returned from the back end. This allows us to use a new feature of <span class="external-link"><a href="https://github.com/facebook/relay" target="_blank">Relay<i data-feather='external-link'></i></a></span> to express which rendering code is needed, depending on what type of data is returned. If the post has a special attachment, such as a photo, we describe that we need the PhotoComponent in order to render that photo. > 那么在整个页面加载过程中,不是静态的代码分支怎么办?例如,将所有不同类型和组合的组件代码全部加载会大大增加页面的JavaScript大小。 > > 这些依赖关系是在运行时根据后端返回的数据类型来决定的。我们使用Relay[4]的一个新功能,根据返回的数据类型来表达需要哪些渲染代码。如果帖子都有一个附件,比如说照片,我们可以用下面声明的方式来描述需要 PhotoComponent 组件渲染照片。 ```javascript ... on Post { ... on PhotoPost { @module('PhotoComponent.js') photo_data } ... on VideoPost { @module('VideoComponent.js') video_data } } ``` *We express the dependencies needed to render each post type as part of the query.* Even better, the `PhotoComponent` itself describes exactly which data on the photo attachment type it needs as a fragment, which means we can even split out the query logic. > 我们将每个帖子类型所需的依赖关系作为查询的一部分来表达 > > 更赞的是,PhotoComponent 本身就把它需要的照片附件类型的数据精确地描述为片段,这意味我们甚至可以把查询逻辑拆分出来。 ### Using JavaScript budgets to prevent code creep > 使用JavaScript预算来防止代码蠕变 Tiers and conditional dependencies help us deliver just the code necessary for each phase, but we also need to make sure the size of each tier stays under control over time. To manage this, we’ve introduced per-product JavaScript budgets. We set budgets based on performance goals, technical constraints, and product considerations. We allocated page-level budgets and subdivide the page based on product boundaries and team boundaries. Shared infra is added to a carefully curated list and given its own budget. Shared infra counts against all pages’ budgets, but modules within it are free for product teams to use. We also have budgets for code that’s deferred, conditionally loaded, or loaded on interaction. We’ve created additional tooling for each step of the process: * A dependency graph tool makes it easier to understand where bytes are coming from and identify opportunities to decrease code size. * Size monitoring on merge requests displays size regressions/improvements and triggers customizable alerts. * Interactive graphs show historical size and how things have changed between revisions. * Dashboards help us understand the current state of sizes in relation to budgets. > 分层和条件依赖关系可以帮助我们交付每个阶段所需的代码,但我们还需要确保每个层的规模随着时间的推移保持在可控范围内。为了管理这个问题,我们引入了每个产品的**JavaScript预算**。 > > **我们根据性能目标、技术约束、产品考虑制定预算**。同时**根据产品边界和团队边界分配页面级预算**,并根据产品边界和团队边界进行细分。**共享基础设施(Shared infra)**被添加到一个精心筛选的列表中,并给出了自己的预算。共享基础设施会计入所有页面的预算,但其中的模块是免费提供给产品团队使用的。对于延迟加载、有条件加载或交互时加载的代码也有预算。 > > 我们为过程的每一步创建了相关的工具: > > 1. 依赖关系图工具让我们更容易理解字节来自哪里,并识别出减少代码大小的机会。 > 2. 合并请求上的大小监控会显示大小回归 / 改进,并触发可定制的警报。 > 3. 通过交互式图表显示历史大小以及修订之间的变化情况。 > 4. 通过Dashboard帮助我们了解当前的大小与预算的关系。 ## Modernizing data-fetching to get it as early as possible > 尽早实现数据获取(data-fetching)的现代化 As part of this rebuild, we modernized our data-fetching infra on the web. While some features of the old site used Relay and <span class="external-link"><a href="https://graphql.org/" target="_blank">GraphQL<i data-feather='external-link'></i></a></span> for data-fetching, most fetched data ad-hoc as part of their server-side PHP rendering. With the new site, we were able to standardize with our mobile apps and ensure that all data-fetching goes through GraphQL. Since Relay and GraphQL already handle the “as little as possible” work for us, we just needed to make some changes to support getting the data we needed as early as possible. > 作为这次重写的一部分,我们对网站上的数据获取的基础设施进行了现代化改造。虽然旧网站的一些功能使用 Relay 和 GraphQL[5] 进行数据采集,但大部分数据获取都是作为服务器端 PHP 渲染的一部分。在新网站上,我们能够与我们的移动应用标准化,并确保所有的数据获取都通过GraphQL进行。由于Relay和GraphQL已经为我们处理了 "尽可能少的 "工作,我们只需要做一些改变,以支持尽早获得我们所需要的数据。 ### Preloading data on the initial server request to improve startup > 初始请求预加载数据,以提高启动效率 Many web apps need to wait until all their JavaScript is downloaded and executed before fetching data from the server. With Relay, we know statically what data the page needs. This means that as soon as our server receives the request for a page, it can immediately start preparing the necessary data and download it in parallel with the required code. We stream this data with the page as it becomes available so the client can avoid additional round trips and render the final page content sooner. > 许多Web应用程序需要等到所有的JavaScript被下载并执行后才从服务器上获取数据。有了Relay,我们可以静态地知道页面需要什么数据。这意味着,一旦我们的服务器收到页面的请求,它就可以立即开始准备必要的数据,并与所需的代码并行下载。当页面可用时,我们会将这些数据与页面一起流转,这样客户端就可以避免额外的往返次数,更快地呈现最终的页面内容。 ### Streaming data for fewer round trips and better interactivity > 为减少往返次数和提高互动性的流数据 On the initial load of Facebook.com, some content may initially be hidden or rendered outside of the viewport. For example, most screens fit one or two News Feed posts, but we don’t know in advance how many will fit. Additionally, it’s very likely the user will scroll, and it would take time to fetch each story individually in a serial round trip. On the other hand, the more stories we fetch in one query, the slower that query gets, which leads to longer query times and a longer Visually Complete time for even the first story. To solve this, we use an internal GraphQL extension, `@stream`, to stream the feed connection to the client both for initial load and subsequent pagination on scroll. This allows us to send each feed story as soon it’s ready, one by one, with just a single query operation. > (注:流数据具有四个特点:数据实时到达;数据到达次序独立,不受应用系统所控制;数据规模宏大且不能预知其最大值;数据一经处理,除非特意保存,否则不能被再次取出处理,或者再次提取数据代价昂贵。(来自网上的解释)) > > 在最初加载Facebook.com时,有些内容可能会被隐藏或呈现在视口之外。例如,大多数屏幕上可以容纳一到两个News Feed帖子,但我们不知道事先会容纳多少个。此外,用户很有可能会滚动,在连载往返的过程中,逐一抓取每个故事需要时间。另一方面,我们在一次查询中获取的故事越多,查询的速度就越慢,这就导致查询时间越长,即使是第一个故事,也需要更长的视觉完成(Visually Complete)时间。 > > (注:视觉完成时间是指网页可见区域内的所有元素都被100%加载。) > > 为了解决这个问题,我们使用了一个内部的GraphQL扩展—**@stream**,将Feed连接流向客户端,用于初始加载和后续滚动时的分页。这使得我们可以在每一个feed故事准备好后,只需进行一次查询操作,就可以将每一个feed故事逐一发送。 ```javascript fragment HomepageData on User { newsFeed(first: 10) { edges @stream } ...AdditionalData } ``` ### Deferring data that’s not needed right away > 推迟暂不需要的数据 Different parts of certain queries take longer to compute than others. For example, when viewing a profile, it’s relatively quick to fetch a person’s name and profile photo, but it takes a bit longer to fetch the contents of their Timeline. To fetch both types of data with a single query, we use `@defer`, which enables different sections of the response to be streamed as soon as they’re ready. This lets us render the bulk of the UI with initial data as quickly as possible, and render loading states for the rest. With <span class="external-link"><a href="https://reactjs.org/docs/concurrent-mode-suspense.html" target="_blank">React Suspense<i data-feather='external-link'></i></a></span>, this is even easier, as we can explicitly craft our loading states to ensure a smooth, top-down page load experience. > 不同部分的查询时间是不同的,例如,在查看个人资料时,获取一个人的姓名资料和照片相对来说比较快,但获取他们的Timeline内容则需要较长的时间。 > > 为了在一次查询中获取这两种类型的数据,我们使用**@defer**,当响应的不同部分准备好后就可以将其变成流数据。这让我们能够尽快用初始数据渲染大部分的UI,并为其余部分渲染加载状态。有了React Suspense[6]就更容易了,因为我们可以显式地设计加载状态,以确保流畅的、自上而下的页面加载体验。 ```javascript fragment ProfileData on User { name profile_picture { ... } ...AdditionalData @defer } ``` ## ## Route maps and definitions for faster navigation > 定义路由图加快导航速度 Fast navigation is an important feature of single-page applications. When navigating to a new route, we need to fetch various code and data from the server to render the destination page. To reduce the number of network round trips required when loading a new page, the client needs to know which resources will be needed for each route ahead of time. We call this a route map and each entry a route definition. > 快速导航是单页应用的一个重要功能。当导航到一个新的路径时,我们需要从服务器上获取各种代码和数据来渲染目的页面。为了减少加载新页面时需要的网络往返次数,客户端需要提前知道每条路线需要哪些资源。我们将其称为路由图,每个条目称为路由定义。 ### Getting route definitions as early as possible > 尽早获得路由定义 For Facebook, this route map is too large to send all at once. Instead, we dynamically add route definitions to the route map during the session, as new links are rendered. The route map and the router live at the very top of the application, allowing the combination of current application and router state to drive app-level state decisions, such as the behavior of the top navigation bar or chat tabs based on the current route. > 对于Facebook来说,这个路由图太大了,无法一次性发送全部的。相反,我们在会话期间,随着新链接的呈现,动态地将路由定义添加到路由图中。路由图和路由器存在应用的最顶端,允许结合当前应用和路由器的状态来驱动应用级的状态决策,例如基于当前路由的顶部导航栏或聊天标签的行为。 ### Prefetching resources as early as possible > 尽早预获取资源 It’s common for client-side applications to wait until a page is being rendered by React to download the code and data needed for that page. Often this is done using <span class="external-link"><a href="https://reactjs.org/docs/code-splitting.html#reactlazy" target="_blank">React.lazy<i data-feather='external-link'></i></a></span> or a similar primitive. Since this can make page navigation slow, we instead start our first request for some of the necessary resources even before a link is clicked: > 客户端应用程序通常要等到React渲染一个页面后才会下载该页面所需的代码和数据。通常情况下使用React.lazy[7]或类似的东西实现。由于这可能会使页面导航速度变慢,所以我们反而会在链接被点击之前就开始请求一些必要的资源。 <img src="https://engineering.fb.com/wp-content/uploads/2020/05/Comet-01.jpg" alt="We kick off fetches early, preloading on hover or focus, and fetching on mousedown. This example is specific to desktop, but other heuristics can be used for touch devices." />We kick off fetches early, preloading on hover or focus, and fetching on mousedown. This example is specific to desktop, but other heuristics can be used for touch devices. To provide a more fluid experience than just showing a blank screen when navigating, we use <span class="external-link"><a href="https://reactjs.org/docs/concurrent-mode-patterns.html#transitions" target="_blank">React Suspense transitions<i data-feather='external-link'></i></a></span> to continue rendering the previous route until the next route is either fully rendered or suspends into a “good” loading state with UI skeletons for the next page. This is much less jarring, and it mimics standard browser behavior. > 为了提供更流畅的体验,我们使用React Suspense转场[8]来继续渲染上一个路由,直到下一个路由完全渲染完毕或暂停到下一个页面的UI骨架的 “友好 “的加载状态。这样做会减少很多干扰,而且它模仿了标准的浏览器行为。 ### Parallelizing code and data download > 代码和数据并行下载 We do a lot of <span class="external-link"><a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading" target="_blank">lazy loading<i data-feather='external-link'></i></a></span> of code on the new site, but if we lazy load the code for a route and the data-fetching code for that route lives inside of that code, we end up with a serial load. > 在新网站上我们做了很多懒加载[9]代码,但如果我们懒加载一个路由的代码,而这个路由的数据抓取代码就在这个路由的代码里面,最后就会出现串行加载的情况。 <img src="https://engineering.fb.com/wp-content/uploads/2020/05/Comet-02.jpg" alt="A “traditional” React/Relay app with lazy loaded routes results in two round trips." />A “traditional” React/Relay app with lazy loaded routes results in two round trips. To solve this problem, we came up with EntryPoints, which are files that wrap a code-split point and transform inputs into queries. These files are very small and are downloaded in advance for any reachable code-split point. > ("传统 "的React / Relay app,加上懒加载的路由,结果会是两次往返) > > 为了解决这个问题,我们想出了**EntryPoints**,它是包裹代码分割点并将输入转化为查询的文件。这些文件非常小,对于任何可以到达的代码拆分点都会提前下载。 <img src="https://engineering.fb.com/wp-content/uploads/2020/05/Comet-03-1.jpg" alt="Code and data are fetched in parallel, allowing us to download these in a single network round trip." />Code and data are fetched in parallel, allowing us to download these in a single network round trip. The GraphQL query is still colocated with the view, but the EntryPoint encapsulates when that query is needed and how to transform the inputs into the correct variables. The app uses these EntryPoints to automatically decide when to fetch the resources, making sure the right thing happens by default. This has the added benefit of creating a single JavaScript function that contains all the data-fetching needs for any given point in the app, which can be used for the server preloading discussed earlier. Many of the changes we’ve discussed here are not specific to Facebook. These concepts and patterns can be applied to any client-side app using any framework or library. By standardizing our tech stack, we have been able to rethink how we introduce functionality that people want in a performant, sustainable way — even as we operate at engineering and product scale. Engineering experience improvements and user experience improvements must go hand in hand, and performance and accessibility cannot be viewed as a tax on shipping features. With great APIs, tools, and automation, we can help engineers move faster and ship better, more performant code at the same time. The work done to improve performance for the new Facebook.com was extensive and we expect to share more on this work soon. To check out the redesign, visit <span class="external-link"><a href="http://facebook.com/new" target="_blank">facebook.com/new<i data-feather='external-link'></i></a></span>. It’s rolling out gradually and will be available to everyone soon. > GraphQL查询仍然与视图写在一起,但EntryPoint封装了何时需要该查询以及如何将输入转化为正确的变量。应用程序使用这些 EntryPoints 来自动决定何时请求,确保默认情况下正确的发生。这有一个额外的好处,那就是创建一个单一的JavaScript函数,它包含了App中任何给定点的所有数据获取需求,可以用于前面讨论的服务器预加载。 > > 我们在这里讨论的许多变化并不是Facebook特有的。这些概念和模式可以应用到任何框架或库的客户端应用程序中。通过标准化我们的技术栈,我们已经能够重新思考如何以一种执行力强、可持续的方式引入人们想要的功能--即使是在工程和产品规模的运营过程中也是如此。 > > 工程体验的改善和用户体验的改善必须齐头并进,不能把性能和可访问性看作是对输出功能的额外负担。通过优秀的API、工具和自动化,我们可以帮助工程师们更快地推进工作,并同时发布更好的、更高性能的代码。为提高新的Facebook.com的性能所做的工作非常广泛,我们预计很快会分享更多关于这项工作的信息。要查看重新设计的内容,请访问facebook.com。它正在逐步推出,很快就会对大家开放。 1. <span class="external-link"><a href="https://engineering.fb.com/web/facebook-redesign/" target="_blank">https://engineering.fb.com/web/facebook-redesign/<i data-feather='external-link'></i></a></span> 2. <span class="external-link"><a href="https://www.yuque.com/docs/share/6aee9dd5-da3f-462b-b4bd-caec0ec6f60e" target="_blank">https://www.yuque.com/docs/share/6aee9dd5-da3f-462b-b4bd-caec0ec6f60e<i data-feather='external-link'></i></a></span> 3. <span class="external-link"><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/font-size" target="_blank">rems<i data-feather='external-link'></i></a></span> 4. <span class="external-link"><a href="https://github.com/facebook/relay" target="_blank">Relay<i data-feather='external-link'></i></a></span> 5. <span class="external-link"><a href="https://graphql.org/" target="_blank">GraphQL<i data-feather='external-link'></i></a></span> 6. <span class="external-link"><a href="https://reactjs.org/docs/concurrent-mode-suspense.html" target="_blank">React Suspense<i data-feather='external-link'></i></a></span> 7. <span class="external-link"><a href="https://reactjs.org/docs/code-splitting.html#reactlazy" target="_blank">React.lazy<i data-feather='external-link'></i></a></span> 8. <span class="external-link"><a href="https://reactjs.org/docs/concurrent-mode-patterns.html#transitions" target="_blank">React Suspense转场<i data-feather='external-link'></i></a></span> 9. <span class="external-link"><a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading" target="_blank">懒加载<i data-feather='external-link'></i></a></span> Last modification:July 28th, 2020 at 02:46 pm © 允许规范转载 Support 如果觉得我的文章对你有用,请随意赞赏 ×Close Appreciate the author Sweeping payments Pay by AliPay Pay by WeChat