一开始,我们先来看看,为什么网站性能如此主要(在本文末端附有本案例研究的链接):
用户体验:糟糕的性能导致无相应,从 UI 和 UX 角度来看,这可能会让用户感到沮丧。转化率和收入:常日,相应缓慢的网站会导致客户流失落,并对转化率和收入产生不好的影响。SEO:从2019 年 7 月 1 日开始,谷歌将对所有新网站默认启用移动优先索引。如果网站在移动设备上的相应缓慢,并且没有适宜移动设备的内容,那么它们的排名会降落。在本文中,我们将简要先容帮助我们提高页面性能的以下几个紧张方面:
针对某些情形,我们的主页是用 React(TypeScript)、Phoenix(Elixir)、Puppeteer(无头 Chrome)和GraphQL API(Ruby on Rails )构建的。在移动设备上的界面如下所示:

Universe homepage 和 explore
性能丈量没有数据,只不过是空谈。—— W. Edwards Deming实验室测试工具(Lab instruments)
实验室测试工具许可在受控环境中,用预定义设备和网络设置采集数据。借助这些工具,调试任何性能问题和具有良好重现性的测试就变得更加大略。
Lighthouse是在本地打算机上审核 Chrome 页面的出色工具。它还供应一些关于如何提高性能、可访问性、SEO 等有用技巧。下面是一些仿照 Fast 3G 和 4 倍 CPU 减速的 Lighthouse 性能审核报告:
用 First Contentful Paint (FCP) 提高 10 倍性能的前后对照
然而,只利用实验室测试工具的缺陷是:它们不一定能创造真实天下的瓶颈问题,这些问题可能取决于终端用户的设备、网络、位置和很多其他成分。这便是为什么利用现场测试工具也很主要的缘故原由。
现场测试工具(Field instrument)
现场测试工具使我们可以仿照和丈量真实的用户页面负载。有很多有助于从实际设备中获取真实性能数据的做事:
WebPageTest——许可在不同位置的实际设备上实行来自不同浏览器的测试。Test My Site——利用基于 Chrome 利用情形统计的 Chrome 用户体验报告(Chrome User Experience Report,简称CrUX);它对公众年夜众开放,并每月更新一次。PageSpeed Insights——结合了实验室(Lighthouse)和现场(CrUX)数据。WebPageTest 报告
渲染渲染内容的方法有很多,每种方法都有其优缺陷:
做事器端渲染(Server-side rendering,简称 SSR)是在做事器端为浏览器获取终极 HTML 文档的过程。优点:搜索引擎可以爬取网站而不实行 JavaScript(SEO)、快速初始页面加载、代码只存在于做事器端。缺陷:没有丰富的网站交互、重新***全体页面,对浏览器功能的访问有限定。客户端渲染是利用 JavaScript 在浏览器中渲染内容的过程。优点:丰富的网站交互、在初始***后根据路由变革快速渲染、可以访问当代浏览器功能(如,Service Workers的离线支持)。缺陷:不支持 SEQ、初始页面加载速率慢、常日须要在做事器端实行单页面运用程序(Single Page Application、简称 SPA)和 API。预渲染和做事器端渲染类似,但是它是在构建期间提前发生的,而不是在运行时发生的。优点:做事构建静态文件常日比运行做事器大略、支持 SEO、初始页面加载快。缺陷:如果代码有任何变革,须要预先预渲染所有可能的页面、重新加载全体页面、站点交互不足丰富、访问浏览器功能受限。客户端渲染
之前,我们把我们的主页和 Ember.js 框架一起实现为具有客户端渲染的 SPA。我们碰着的一个问题是,Ember.js 运用程序包太大。这意味着,在浏览器***、解析、编译和实行 JavaScript 文件时,用户只能看到一个空缺的屏幕。
白屏
我们决定用React重修该运用程序的某些部分。
我们的开拓职员已经熟习构建 React 运用程序(如,嵌入式小部件)。我们已经有了一些 React 组件库,可以在多个项目之间共享它们。新页面有一些交互式 UI 元素。有一个拥有大量工具的弘大的 React 生态系统。借助浏览器中的 JavaScript,可以构建具有大量良好功能的渐进式 web 运用程序(Progressive Web App)。预渲染和做事器端渲染
例如,用React Router DOM构建的客户端渲染运用程序的问题, 仍旧和 Ember.js 的相同。JavaScript 开销大,并且须要一些韶光才能看到浏览器中的首次内容绘制(First Contentful Paint)。
当我们决定利用 React 后,我们立时就用其它潜在的渲染选项进行试验,以让浏览器更快地渲染内容。
利用 React 的常规渲染选项
Gatsby.js使我们可以用 React 和 GraphQL 预渲染页面。Gatsby.js 是个很棒的工具,它支持很多开箱即用的性能优化。然而,对我们来说,预渲染没有用,由于我们可能有无数个页面,它们包含用户天生的内容。Next.js是盛行的 Node.js 框架,它许可做事器端用 React 渲染。然而,Next.js 很自我,须要利用其路由、CSS 办理方案等等。我们现有的组件库是为浏览器而构建的,与 Node.js 不兼容。
这便是我们为什么决定考试测验一些稠浊方法的缘故原由,考试测验从每个渲染选项中得到最佳效果。
运行时预渲染
Puppeteer是个 Node.js 库,它许可利用无头 Chrome。我们希望让 Puppeteer 试试在运行时进行预渲染。这支持利用一种有趣的稠浊方法:做事器端用 Puppeteer 渲染,客户端用激活渲染。这里有一些谷歌供应的有用窍门,关于如何利用无头浏览器来进行做事器端渲染。
用于运行时预渲染 React 运用程序的 Puppeteer
利用这种方法有如下优点:
可以利用 SSR,对 SEO 来说,这很棒。爬虫程序不须要实行 JavaScript 就能看到内容。许可构建大略浏览器 React 运用程序一次,然后把它用在做事器端和浏览器中。让浏览器运用程序更快田主动让 SSR 更快,这是双赢。在做事器上用 Puppeteer 渲染页面常日比在终端用户的移动设备上更快(连接更好, 硬件更好)。激活许可用对 JavaScript 浏览器功能的访问来构建丰富的 SPA。我们无需事先知道所有可能的页面来预渲染它们。然而,我们在利用这个方法时碰着了一些寻衅:
吞吐量是紧张问题。在单独的无头浏览器进程中实行每个要求花费了大量资源。你可以利用单个无标题浏览器进程,并在单独的选项卡中运行多个要求。然而,利用多个选项卡将会使全体进程的性能低落了。利用 Puppeteer 进行做事器端渲染的体系构造
稳定性。扩展或缩小很多无头浏览器,让流程保持“热度”及平衡事情负载是个寻衅。我们考试测验了不同的托管方法:从 Kubernetes 集群自托管到用 AWS Lambda 和 Google Cloud Functions 的无做事器。我们把稳到,后者在用到 Puppeteer 时有一些性能问题:
在 AWS Lambdas 和 GCP 函数上的 Puppeteer 相应韶光
随着我们越来越熟习 Puppeteer,我们已经迭代了我们的初始方法(如下所示)。我们还进行着一些有趣实验,通过一个无头浏览器来渲染 PDF。还可以利用 Puppeteer 来进行自动端到端测试,乃至都不用写任何代码。现在,除了 Chrome,它还支持 Firefox。
稠浊渲染方法
在运行时利用 Puppeteer 很具寻衅性。这是我们为什么决定在构建时利用它,并借助一个在运行时可以从做事器端返回实际用户天生内容的工具。与 Puppeteer 比较,它更稳定,并且吞吐量更大。
我们决定考试测验一下 Elixir 编程措辞。Elixir 看起来像 Ruby,但是运行于 BEAM(Erlang VM)之上,旨在构建容错且稳定的系统。
Elixir 利用Actor 并发模型。每个“Actor”(Elixir process)只占用很少的内存,约为 1-2KB。这样许可同时运行数千个独立进程。Phoenix是一个 Elixir web 框架,支持高吞吐量,并在独立的 Elixir 过程中处理每个 HTTP 要求。
我们结合了这些方法,充分利用了它们各自的优点,知足了我们的须要:
Puppeteer 用于预渲染,而 Phoenix 用于做事器端渲染
Puppeteer在构建时用我们希望的办法预渲染 React 页面,并以 HTML 文件形式保存它们(App Shell 来自PRPL 模式)。
我们可以连续构建一个大略的浏览器 React 运用程序,不须要在终端用户设备上等待 JavaScript 就可以快速加载初始页面。
我们的Phoenix运用程序做事于这些预渲染页面,并动态地把实际内容注入到 HTML 中。这让内容 SEO 变得很友好,许可根据须要处理大量不同的页面,并且更随意马虎扩展。
客户端吸收并立即显示 HTML,然后引发Recat DOM状态以连续作为常规 SPA。这样,我们可以构建高度交互的运用程序,和访问 JavaScript 浏览器功能。
利用 Puppeteer 进行预渲染、利用 Phoenix 进行做事器端渲染和引发利用 React
网络
内容分发网络(CDN)
利用 CDN 可以实现内容缓存,并可以加速其在世界范围内的分发。我们利用Fastly.com,它为超过 10% 的互联网要求供应做事,并为各种公司利用,如 GitHub、Stripe、Airbnb、Twitter 等等。
Fastly 许可我们通过利用名为VCL的配置措辞编写自定义缓存和路由逻辑。下图显示了一个基本要求流的事情事理,根据路由、要求标头等等来自定制每个步骤:
VCL 要求流
另一个提高性能的选择是在边缘利用 WebAssembly(WASM)和 Fastly。把它想象成利用无做事器,但是在边缘利用这些编程措辞,如 C、Rust、Go、TypeScript 等等。Cloudflare 有个类似的项目支持Workers上的 WASM.
缓存尽可能多地缓存要求对提高性能很主要。CDN 级别上的缓存可以更快地为新用户供应相应。通过发送 Cache-Control 头来缓存可以加快浏览器中重复要求的相应韶光。
大多数构建工具(如Webpack)许可给文件名添加哈希值。可以安全地缓存这些文件,由于变动文件将创建新的输出文件名。
通过 HTTP/2 缓存和编码的文件
GraphQL 缓存
发送 GraphQL 要求最常见的方法之一是利用 POST HTTP 方法。我们利用的一种方法是在 Fastly 级缓存一些 GraphQL 要求:
我们的 React 运用程序注释了可以缓存的 GraphQL 要求。发送 HTTP 要求前,我们通过从要求正文构建哈希值来附加 URL 参数,该要求正文包括 GraphQL 要乞降变量(我们利用Apollo Client自定义 fetch)。默认情形下,Varnish(和 Fastly)利用全体 URL 作为缓存键的一部分。这许可我们连续在要求正文中利用 GraphQL 查询发送 POST 要求,并在边缘缓存,而不会访问我们的做事器。发送带有 SHA256 URL 参数的 POST GraphQL 要求
以下是一些其它潜在的 GraphQL 缓存策略:
在做事器端缓存:全体 GraphQL 要求,在解析器级别上或通过注释模式声明性地进行缓存。利用持久的 GraphQL 查询和发送 GET/graphql/:queryId 以便能够依赖 HTTP 缓存。通过利用自动化工具(如Apollo Server 2.0)或利用 GraphQL 特定的 CDN(如FastQL)与 CDN 集成。编码所有主流浏览器都支持带有Content-Encoding头的 gzip 来压缩数据。这可以让我们给浏览器发送的字节更少,这常日意味着内容通报会更快。如果浏览器支持的话,你还可以利用更有效的 brotli 压缩算法。
HTTP/2 协议
HTTP/2是 HTTP 网络协议(在 DevConsole 中是 h2)的新版本。切换到 HTTP/2 可以提升性能,这归结于它和 HTTP/1.x 的这些不同之处:
HTTP/2 是二进制的,不是文本。解析更高效,更紧凑。HTTP/2 是多路复用的,这意味着 HTTP/2 可以通过单个 TCP 连接并行发送多个要求。它让我们不用担心每个主机限定和域分片的浏览器连接。它利用头压缩来减少要求 / 相应大小开销。许可做事器主动推送相应。该功能相称有趣。HTTP/2 做事器推送
有很多编程措辞和库并不完备支持所有 HTTP/2 功能,缘故原由是它们为现有工具和生态系统(如,rack)引入了毁坏性变动。但是,纵然在这种情形下,仍旧可以利用 HTTP/2,至少可以部分利用。如:
在常规 HTTP/1.x 做事器前利用 HTTP/2 设置代理做事器,如h2o或nginx。例如 Puma 和 Rails 上的 Ruby 可以发送Early Hints,这可以启用 HTTP/2 做事器推送,但受到一些限定。利用支持 HTTP/2 的 CDN 供应静态资产。例如,我们用这种方法给客户端推送字体和一些 JavaScript 文件。HTTP/2 推送字体
推送关键的 JavaScript 和 CSS 也可以很有用。只是不要过度推送,并戒备某些陷阱。
浏览器中的 JavaScript
包大小的预算
第一条 JavaScript 性能规则是不要利用 JavaScript。我这么认为。如果我们已经有现成的 JavaScript 运用程序,那么设置预算可以改进包大小的可见性,并让所有人都勾留在同一个页面上。超预算迫使开拓职员三思而后行,并把规模的增加掌握在最小程度。关于如何设置预算,在此举几个例子:
根据我们的须要或一些推举值利用数字。例如,小于 170KB的缩小和压缩的 JavaScrip。把当前的包大小作为基准,或考试测验把它减少,例如 10%。试试我们的竞争对手中最快的网站,并相应地设置预算。我们可以利用 bundlesize 包或 Webpack 性能提示和限定来追踪预算:
Webpack 性能提示和限定
删除依赖项这是由 Sidekiq 的作者所写的一篇热门博文的标题
没有代码能比没代码运行得更快。没有代码能比没代码有更少的缺点。没有代码能比没代码利用更少的内存。没有代码能比没代码更随意马虎让人理解。不幸的是,JavaScript 依赖项的现实是,我们的项目很有可能利用数百个依赖项。试试 Is node_modules | wc -l。
在某些情形下,添加依赖项是必须的。在这种情形下,依赖项包的大小该当是在多个包之间进行选择时的标准之一。我强烈推举利用BundlePhobia:
BundlePhobia 创造向包中添加 npm 包的本钱
代码拆分利用代码拆分可能是显著提高 JavaScript 性能的最佳方法。它许可拆分代码,并只通报用户当前须要的那部分。以下是一些代码拆分的例子:
在单独的 JavaScript 块等分别加载路由页面上可以不立即显示的组件,例如,在页面下方的模态、页脚。在所有主流浏览器中,polyfills和ponyfills都支持最新的浏览器功能。通过利用 Webpack 的 SplitChunksPlungin,避免代码重复。根据须要定位文件,以避免一次性发送所有我们支持的措辞。借助 Webpack动态导入和具有Suspense的React.lazy,我们可以利用代码拆分。
借助动态引入和具有 Suspense 的 React.lazy 的代码拆分
我们构建了一个取代 React.lazy 的函数来支持命名导出,而不是默认导出。
异步和延迟脚本
所有主流浏览器支持脚本标签上的异步和延迟属性
加载 JavaScript 的不同方法内联脚本对付加载小型关键 JavaScript 代码非常有用。当用户或任何其他脚本(例如,剖析脚本)不须要该脚本,要获取 JavaScript 而不妨碍 HTML 解析时,利用带async的脚本非常有用。从性能的角度看,要获取和实行非关键 JavaScript,并且不阻碍 HTML 解析,那么,利用带defer的脚本可能是最佳方法。此外,它确保调用脚本时的实行顺序,如果一个脚本依赖另一个脚本,那么这个方法会很有用。
以下显示了在头标签中这些脚本之间的差异:
脚本获取和实行的不同方法
图像优化只管 JavaScript 的 100KB 与图像的 100KB 比较,性能本钱有很大的不同,但是,常日来说,只管即便让图像保持比较小的文件大小很主要。
一种减小图像大小的方法是,在受支持的浏览器中利用更轻量级的WebP图像格式。对付那些不支持 WebP 的浏览器来说,可以利用以下策略:
退回到常规 JPEG 或 PNG 格式(一些 CDN 根据浏览器的 Accept 要求头自动实行)在检测到浏览器支持后,加载并利用WebP polyfill。利用 Service Workers 来侦听以获取要求,如果 WebP 受到支持,那么就变动实际的 URL 以利用 WebP。WebP 图像
仅当图像在位于或靠近视图端口时才延迟加载图像,对付具有大量图像的初始页面加载来说,这是最显著的性能改进之一。我们可以在支持的浏览器中利用 IntersectionObserver功能,或利用一些可更换的工具来实现同样的结果,例如,react-lazyload。
在滚动期间延迟加载图像
其他一些图像优化可能包括:降落图像的质量以减少图像的尺寸。调度大小并尽可能加载最小的图像。利用srcset图像属性为高分辨率视网膜显示器自动加载高质量图像。利用渐进式图像,先立即显示出模糊的图像加载常规图像和渐进图像的比拟
我们可以考虑利用一些通用 CDN 或专用图像 CDN,它们常日实现了这些图像优化的大部分事情。
资源提示
资源提示让我们可以优化资源的交付,减少来回次数,以及资源的获取,以便在用户浏览页面时更快地通报内容。
带有 link 标记的资源提示
预加载(preload)在当前页面加载的后台***资源,并会实际用于当前页面(高优先级)。预取(prefetch)的事情事理和预加载类似,都是获取资源并缓存它们,但用于未来用户的导航(低优先级)。预连接(preconnect)许可在 HTTP 要求在实际发送到做事器之前,设置早期连接。提前预连接以避免 DNS、TCP 和 TLS 来回延迟
还有其他一些资源提示,如预渲染或DNS 预取。个中有一些可以在相应头上指定。在利用资源提示时,请小心行事。很随意马虎一开始就造成太多不必要的要乞降***太多数据,特殊是如果用户在利用蜂窝连接。
结论在不断增长的运用中,性能是永无止境的过程,该过程常日须要在全体栈中不断变动。
这个***提醒我,大家希望减少运用程序包的大小——我的同事把统统你现在不须要的东西都扔出飞机!
——电影《珍珠港》
以下是一个列表,表中是我们在利用或操持考试测验的其他未提及的潜在性能改进:
利用 Service Workers 进行缓存、脱机支持及卸载主线程。内联关键 CSS 或利用功能性 CSS,以便长期减小尺寸大小。利用如 WOFF2 而不是 WOFF 的字体格式(最高可压缩一半大小)。浏览器列表保持更新。利用webpack-bundle-analyzer进行构建块的可视化剖析。优选较小的包(例如,date-fns)和许可减小尺寸大小的插件(如,lodash-webpack-plugin)。试试preact、lit-html或svelte。在CI 中运行 Lighthouse。渐进引发和用React进行流处理。令人愉快的想法无穷无尽,我们都可以拿来考试测验。我希望这些信息和这些案例研究可以启示大家去思考运用程序中的性能。
据亚马逊打算,页面***速率每低落 1 秒就可能造成年发卖额减少 13 亿美元。沃尔玛创造,加载韶光每减少 1 秒,将使转换量增加 2%。每 100ms 的改进还会带来高达 1% 的收入增加。据谷歌打算,搜索结果每放慢 0.4 秒,那么每天的搜索次数有可能减少 8 百万次。重构 Pinterest 页面的性能使等待韶光减少了 40%,而 SEO 流量增加了 15%,注册转化率增加了 15%。BBC 创造,其网站加载韶光每增加一秒,就会多流失落 10% 的用户。对新的更快的 FT.com 的测试表明,用户参与度提高了 30%,这意味着更多的访问次数和更多的内容消费。Instagram 通过减少显示评论所需 JSON 的相应大小,将展示次数和用户个人资料滚动互动量增加了 33%。点击“理解更多”,获取更多优质阅读