浏览器家园·资讯

展开

浏览器性能提升 10 倍的经历

编辑:浏览器知识

作者:exAspArk
翻译:姚佳灵

最近,我们把 Universe.com 的主页性能提高了 10 几倍。让我们一起来探索一下我们是如何实现这个结果的,涉及到了哪些技术。

一开始,我们先来看看,为什么网站性能如此重要(在本文末尾附有本案例研究的链接):

在本文中,我们将简要介绍帮助我们提高页面性能的以下几个主要方面:

针对某些情况,我们的主页是用 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 报告

渲染

渲染内容的方法有很多,每种方法都有其优缺点:

客户端渲染

之前,我们把我们的主页和 Ember.js 框架一起实现为具有客户端渲染的 SPA。我们遇到的一个问题是,Ember.js 应用程序包太大。这意味着,在浏览器下载、解析、编译和执行 JavaScript 文件时,用户只能看到一个空白的屏幕。

白屏
我们决定用 React 重建该应用程序的某些部分。

预渲染和服务器端渲染

例如,用 React Router DOM 构建的客户端渲染应用程序的问题, 仍然和 Ember.js 的相同。JavaScript 开销大,并且需要一些时间才能看到浏览器中的首次内容绘制(First Contentful Paint)。

当我们决定使用 React 后,我们马上就用其它潜在的渲染选项进行试验,以让浏览器更快地渲染内容。

使用 React 的常规渲染选项

这就是我们为什么决定尝试一些混合方法的原因,尝试从每个渲染选项中获得最佳效果。

运行时预渲染

Puppeteer 是个 Node.js 库,它允许使用无头 Chrome。我们希望让 Puppeteer 试试在运行时进行预渲染。这支持使用一种有趣的混合方法:服务器端用 Puppeteer 渲染,客户端用激活渲染。这里有一些谷歌提供的有用窍门,关于如何使用无头浏览器来进行服务器端渲染。

用于运行时预渲染 React 应用程序的 Puppeteer

使用这种方法有如下优点:

然而,我们在使用这个方法时遇到了一些挑战:

使用 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 用于服务器端渲染

我们可以继续构建一个简单的浏览器 React 应用程序,不需要在终端用户设备上等待 JavaScript 就可以快速加载初始页面。

这让内容 SEO 变得很友好,允许根据需要处理大量不同的页面,并且更容易扩展。

这样,我们可以构建高度交互的应用程序,和访问 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 请求:

发送带有 SHA256 URL 参数的 POST GraphQL 请求

以下是一些其它潜在的 GraphQL 缓存策略:

编码

所有主流浏览器都支持带有 Content-Encoding 头的 gzip 来压缩数据。这可以让我们给浏览器发送的字节更少,这通常意味着内容传递会更快。如果浏览器支持的话,你还可以使用更有效的 brotli 压缩算法。

HTTP/2 协议

HTTP/2 是 HTTP 网络协议(在 DevConsole 中是 h2)的新版本。切换到 HTTP/2 可以提升性能,这归结于它和 HTTP/1.x 的这些不同之处:

HTTP/2 服务器推送

有很多编程语言和库并不完全支持所有 HTTP/2 功能,原因是它们为现有工具和生态系统(如, rack )引入了破坏性更改。但是,即使在这种情况下,仍然可以使用 HTTP/2,至少可以部分使用。如:

HTTP/2 推送字体
推送关键的 JavaScript 和 CSS 也可以很有用。只是不要过度推送,并提防某些陷阱。

浏览器中的 JavaScript

包大小的预算

第一条 JavaScript 性能规则是不要使用 JavaScript。我这么认为。

如果我们已经有现成的 JavaScript 应用程序,那么设置预算可以改进包大小的可见性,并让所有人都停留在同一个页面上。超预算迫使开发人员三思而后行,并把规模的增加控制在最小程度。关于如何设置预算,在此举几个例子:

我们可以使用 bundlesize 包或 Webpack 性能提示和限制来追踪预算:

Webpack 性能提示和限制

删除依赖项

这是由 Sidekiq 的作者所写的一篇热门博文的标题

没有代码能比没代码运行得更快。没有代码能比没代码有更少的错误。没有代码能比没代码使用更少的内存。没有代码能比没代码更容易让人理解。

不幸的是,JavaScript 依赖项的现实是,我们的项目很有可能使用数百个依赖项。试试 Is node_modules | wc -l。

在某些情况下,添加依赖项是必须的。在这种情况下,依赖项包的大小应该是在多个包之间进行选择时的标准之一。我强烈推荐使用 BundlePhobia 

BundlePhobia 发现向包中添加 npm 包的成本

代码拆分

使用代码拆分可能是显著提高 JavaScript 性能的最佳方法。它允许拆分代码,并只传递用户当前需要的那部分。以下是一些代码拆分的例子:

借助 Webpack动态导入和具有 Suspense  React.lazy ,我们可以使用代码拆分。

借助动态引入和具有 Suspense 的 React.lazy 的代码拆分

我们构建了一个取代 React.lazy 的函数来支持命名导出,而不是默认导出

异步和延迟脚本

所有主流浏览器支持脚本标签上的异步和延迟属性

加载 JavaScript 的不同方法

以下显示了在头标签中这些脚本之间的差异:

脚本获取和执行的不同方法

图像优化

尽管 JavaScript 的 100KB 与图像的 100KB 相比,性能成本有很大的不同,但是,通常来说,尽量让图像保持比较小的文件大小很重要。

一种减小图像大小的方法是,在受支持的浏览器中使用更轻量级的 WebP 图像格式。对于那些不支持 WebP 的浏览器来说,可以使用以下策略:

WebP 图像
仅当图像在位于或接近视图端口时才延迟加载图像,对于具有大量图像的初始页面加载来说,这是最显著的性能改进之一。我们可以在支持的浏览器中使用 I ntersectionObserver 功能,或使用一些可替换的工具来实现同样的结果,例如, react-lazyload 

在滚动期间延迟加载图像
其他一些图像优化可能包括:

加载常规图像和渐进图像的对比
我们可以考虑使用一些通用 CDN 或专用图像 CDN,它们通常实现了这些图像优化的大部分工作。

资源提示

资源提示让我们可以优化资源的交付,减少往返次数,以及资源的获取,以便在用户浏览页面时更快地传递内容。

带有 link 标记的资源提示

提前预连接以避免 DNS、TCP 和 TLS 往返延迟
还有其他一些资源提示,如预渲染 DNS 预取。其中有一些可以在响应头上指定。在使用资源提示时,请小心行事。很容易一开始就造成太多不必要的请求和下载太多数据,特别是如果用户在使用蜂窝连接

结论

在不断增长的应用中,性能是永无止境的过程,该过程通常需要在整个栈中不断更改。

这个视频提醒我,大家希望减少应用程序包的大小——我的同事

把一切你现在不需要的东西都扔出飞机!——电影《珍珠港》

以下是一个列表,表中是我们在使用或计划尝试的其他未提及的潜在性能改进:

令人兴奋的想法无穷无尽,我们都可以拿来尝试。我希望这些信息和这些案例研究可以启发大家去思考应用程序中的性能。

据亚马逊计算,页面下载速度每下降 1 秒就可能造成年销售额减少 13 亿美元。

沃尔玛发现,加载时间每减少 1 秒,将使转换量增加 2%。每 100ms 的改进还会带来高达 1% 的收入增加。

据谷歌计算,搜索结果每放慢 0.4 秒,那么每天的搜索次数有可能减少 8 百万次。

重构 Pinterest 页面的性能使等待时间减少了 40%,而 SEO 流量增加了 15%,注册转化率增加了 15%。

BBC 发现,其网站加载时间每增加一秒,就会多流失 10% 的用户。

对新的更快的 FT.com 的测试表明,用户参与度提高了 30%,这意味着更多的访问次数和更多的内容消费。

Instagram 通过减少显示评论所需 JSON 的响应大小,将展示次数和用户个人资料滚动互动增加了 33%。

原文链接:
https://engineering.universe.com/improving-browser-performance-10x-f9551927dcff

文章TAG:浏览  浏览器  性能  性能提升  浏览器性能提升  倍的经历  

加载全部内容

相关教程
猜你喜欢
大家都在看