DoorDash高流量网页性能优化的经验教训


随着电子商务平台的发展,DoorDash 的 Web 应用程序开始变慢,并且需要提高性能的技术来保持它们的速度和功能。

在 DoorDash,随着 Web 应用程序的扩展,用户有时不得不等待长达 10 秒的 UI 才能加载——如果网络不好,可能会更长。我们一直在努力改善我们产品的网络性能,以提升用户体验。在这里,我们分享了我们遇到的一些挑战以及我们在此过程中学到的教训。这五个关键行动可以帮助任何大型平台提高其性能指标: 

  • 管理 JavaScript 包大小
  • 延迟在视口外渲染 DOM 元素
  • 通过减少 API 瓶颈来缩短渲染时间
  • 延迟加载渲染阻塞资源
  • 应用正确的缓存策略

管理 JavaScript 包大小
具有大型 JavaScript 包的网站的性能可能会受到阻碍,因为用户将包下载到他们的设备并解压缩、解析和执行包需要更长的时间。作为管理捆绑包大小的一部分,请记住以下几点。

1、如何检测与捆绑包大小相关的性能问题
WebPageTest或 Chrome 的 Lighthouse 开发工具等工具可用于审核页面并查看性能分析报告,其中包括已识别的问题和改进机会。如图 1 所示,“减少未使用的 JavaScript”、“避免过多的 DOM 大小”或“减少 JavaScript 执行时间”等机会表明需要减少站点的捆绑包大小。

2、如何减小捆绑包大小
首先,使用webpack-bundle-analyzer等工具分析包中的内容,以确定哪个组件/模块/库对最终包的贡献最大。或者,使用Chrome 的 Coverage 开发工具来识别当前页面中捆绑包的“未使用”代码。

在此分析之后,可以应用许多技术来减小包大小,包括: 


这些是众所周知的技术,当代码库仍然很小时很容易应用。
但在 DoorDash,消费者网站已经是一个由 60 多名工程师维护的庞大代码库。更改需要团队和功能所有者之间的协调以及足够的手动/自动测试,以确保它们不会破坏高流量页面。考虑到我们的时间/资源限制,这里提到的技术不适合我们的目的,但我们发现了其他容易实现的目标来减小包大小。

通过确保依赖项是可Tree shaking的,获得重大胜利
为了了解性能改进如何提高 DoorDash 消费者的转化率——从在主页上浏览餐厅到成功签出购物车——我们希望找到任何可以减少捆绑大小并快速提高网站性能的容易实现的目标。
我们首先分析了 DoorDash的主包。我们发现了一个名为libphonenumber-js的库,它在我们内部设计系统的许多组件中都有重复。

找到这个库是一个特别有用的发现,因为这些组件中的大多数都很少使用“libphonenumber-js”,如果有的话。相反,libphonenumber-js 在每个设计系统组件中,因为它是设计系统中通用实用程序包 (@doordash/utilities) 的依赖项,而 @doordash/utilities 是几乎所有设计系统组件的依赖项。虽然依赖链导致了重复库被引入到每个组件,但它应该在构建时进行tree-shaking 。
这就是我们发现libphonenumber 直到 1.9.4 版本才支持 tree-shaking. 最后,修复是微不足道的。我们只需要更新设计系统中使用的`libphonenumber-js`的版本,并提升消费者网站存储库中使用的设计系统依赖的版本。
此操作在压缩后从主包中消除了 300KB。在发布此更改后,我们观察到对网络流量的定向积极转换影响约为 0.50%——在我们的规模上,这是显着的。

延迟在视口外渲染 DOM 元素
使用 Lighthouse 工具审核 DoorDash 网页的性能,我们发现网站呈现了 5,000 多个元素,影响了页面的运行时性能。

页面将构建 UI 所涉及的所有繁重工作委托给客户端。页面拥有的元素越多,浏览器必须执行的任务就越多,例如重新计算节点的位置和样式。大型 DOM 树与复杂的样式规则相结合会严重降低渲染速度。

用户希望在访问网页时尽快看到 UI 出现。为了快速加载,页面首先必须渲染更少的元素;不幸的是,我们的网页同时呈现所有元素。为了减少第一次加载时渲染的元素数量,我们对首屏以下的元素应用了延迟加载。我们希望确保页面只会在视口中呈现必要的组件,而推迟其余的。 

在应用延迟加载之前,我们必须考虑它是否会影响我们网站的搜索引擎优化 (SEO) 排名。通常,搜索引擎更喜欢内容丰富的网站,但延迟加载会延迟在视口之外加载内容。如果网络爬虫访问该网站并看到更少的内容,我们的搜索排名可能会受到影响。好在我们审核的网页不需要爬取,也无法直接访问。这意味着延迟加载方法不会损害我们的搜索排名,因此我们能够将其应用于我们的网页。

为了应用延迟加载,我们首先分析了 UI 以确定页面中呈现最多元素的区域。因为页面第一次加载时标题和类别菜单在视口中,并且它们没有很多元素,所以我们决定将延迟加载仅应用于精选区域。

根据首次输入延迟 (FID) 读数,我们看到加载速度显着降低。以下是我们 20 天实验后的结果:

  • FID
    • 移动:67% 改进(从 145.44 毫秒到 48.46 毫秒)
    • 桌面:46% 改进(从 11.46 毫秒到 6.21 毫秒)

通过减少 API 瓶颈来缩短渲染时间
对于 DoorDash 的某些页面,我们使用从 API 返回的配置来控制 UI 的各个部分。这些页面需要等待初始响应才能开始呈现。不幸的是,其中一些端点在内部与许多不同的服务进行通信,以便为客户端创建响应,这意味着渲染可能需要更长的时间。

为了进行调查,我们使用 Chrome DevTools 的性能特性来研究网站的性能:

我们注意到在初始呈现时,在呈现页面上的内容之前有 4 秒的空白。专注于空渲染,我们可以看到该网站进行了几次 API 调用。对于服务器配置驱动的 UI,配置的响应时间对于第一次绘制至关重要。
使用 Chrome DevTools,我们监控了返回 UI 配置的主要 API 的响应时间来验证这一点。

API 需要几秒钟来响应,这会延迟 UI 呈现。

最后,我们被提醒在审查性能时需要衡量 API 响应,并且此问题的一些潜在改进领域包括:

  • 渲染占位符以提供第一个有意义的呈现
  • 为性能更重要的页面呈现不受 API 响应驱动的静态内容
  • 在 SSR 中发出第一个配置请求以提供准确的首次渲染

延迟加载渲染阻塞资源
当用户访问网站时,在页面呈现在屏幕上之前,它必须等待同步下载的资源(如 CSS 样式表或 JavaScript)加载或同步 JavaScript 任务完成执行。当用户可以看到页面时,这些问题可能会延迟。通过消除这些渲染阻塞资源,我们或许能够提高网站性能。
识别渲染阻塞资源
有几种方法可以识别渲染阻塞资源:

  1. 在 Chrome DevTools中使用Lighthouse ,
  2. 检查在“DOMContentLoaded”事件之前加载的那些资源
  3. 在 Chrome DevTools 的网络选项卡的“瀑布”面板中,蓝线表示何时触发了“DOMContentLoaded”事件。只有当页面的初始 HTML 完全解析并构建 DOM 时才会触发此事件;在此事件之前加载的资源(例如 CSS 和 JavaScript)可能会阻塞解析和呈现过程。 

为渲染阻塞资源应用 async/defer
消除渲染阻塞资源的最常用技术是将async 或 defer属性添加到脚本(用于加载 JavaScript)和样式表(用于加载 CSS)标签。这告诉浏览器这些资源并不重要,因此解析器不必等待它们被下载,然后继续解析和呈现。这一次,我们还审核了 DoorDash 网站的每个页面,以确保将正确的“异步”或“延迟”属性应用于那些非关键资源。


尽量减少渲染阻塞资源的负面影响
一些 CSS 和 JavaScript 可能对当前页面至关重要——这会使它们不可避免地出现渲染阻塞——但一些技术可以最大限度地减少负面影响。

  • 提取关键 CSS并将其内联到 `head' 标签中 仅提取页面中实际使用的 CSS 并将它们放入 `head' 标签中以同步加载,然后异步下载样式表的其余部分。使用这种技术,浏览器不必发出额外的请求来获取该页面的关键 CSS,通过仅解析关键 CSS 来减少渲染阻塞时间。在 DoorDash 中,我们使用styled-components来构建 UI,它会自动注入关键的 CSS,这意味着我们不必进行任何更改。

防止长时间运行的脚本执行虽然同步加载的脚本可能会阻塞渲染,但应用了“async异步”属性的脚本在解析器进程完成之前完成加载时也可能会阻塞渲染。在这种情况下,脚本会立即执行,从而阻止解析和渲染。因此,避免将长时间运行的脚本放在“head”标签中很重要。

我们这次审核的长时间运行的脚本

  • Optimizely.js:用于 A/B 测试的同步加载的第三方脚本。官方文档建议不要将其加载为非阻塞以避免页面闪烁,因此我们没有对这个脚本进行任何更改。
  • Analytics.min.js:分段分析脚本的包装器,使用“async”属性异步加载。我们发现我们正在使用版本 1,但版本 2 已经推出以改善执行和加载时间。我们选择不升级,因为我们有一个全公司计划将分析客户端迁移到内部解决方案。
  • Polyfill.js:所有 DoorDash web JavaScript polyfills 都放在这个文件中,它会在很长一段时间内同步加载(在非节流环境中大约 900 毫秒)。但是这个 polyfill 由所有 DoorDash 网站共享,因此我们得出结论,在与相关团队进行适当沟通之前删除任何内容的风险太大。相反,我们没有采取任何行动,并且将依赖于该文件的长缓存过期时间,但在未来,我们可以使用Polyfill.io等工具仅加载请求浏览器所需的 polyfill。

尽管我们没有为这一举措找到立竿见影的效果,但我们相信审核网站是有价值的,列出了未来改进的所有潜在机会。

应用正确的缓存策略
当数据被缓存时,副本被存储在可以比原始源更快地检索的地方。但是,使用缓存数据可能会出现问题,因为它可能与源的更新不同。
用户交互涉及大量数据,包括静态资产(用于呈现网站、使其具有交互性并提供图像内容和字体文件的 HTML、CSS 和 JavaScript 文件)和动态内容,包括用户输入数据,这些数据可以在系统中被创建、更新、读取和删除。这些数据可以通过:

  • 用户浏览器(服务工作者)
  • 应用代码
  • HTTP 协议
  • 内容分发网络
  • 负载均衡器
  • 后端服务器
  • 数据库

数据通过的每个组件都创建了一个添加缓存的机会,以比访问源更快地检索数据。

1、如何审核缓存的配置
正确配置缓存可以显着提高网站性能。简单地在我们上面提到的组件中添加标题或切换组件配置也可能是唾手可得的成果。以下是一些审计缓存配置的技术。 
检查 HTTP 缓存
要确认HTTP 缓存是否按预期工作,请使用 Chrome DevTools 在“大小”选项卡下检查每个静态资产(如 JavaScript 包、图像或字体)的大小。
如果这些资产是从“磁盘缓存”或“内存缓存”提供的,则可能没有问题;浏览器从缓存中获取它们,而不是从 CDN 或服务器获取它们。要深入了解如何为每个资产配置缓存,请检查响应标头中的Cache-ControlExpiresETagLast-Modified的值。

即使没有配置适当的与缓存相关的标头,现代浏览器也足够聪明,可以使用启发式新鲜度来决定是否应该缓存资源。尽管如此,我们建议显式配置它们,而不是依赖启发式新鲜度,这不是确定性的,并且高度依赖于每个浏览器的实现。

2、确认 CDN 中的缓存命中率
CDN缓存和HTTP缓存的区别在于CDN缓存的资产可以在包括浏览器在内的所有客户端之间共享,而HTTP缓存只能被当前用户设备的浏览器使用。要审核 CDN 缓存的配置,请转到 CDN 的仪表板(在 DoorDash 中,我们使用Cloudflare)检查缓存命中率和正在缓存的文件类型。

与 HTTP 缓存一样,CDN 缓存会尊重缓存控制标头设置,因此如果存在不应该经常修改但缓存未命中率高的静态文件,请通过检查缓存控制开始调查环境。

3、确认应用级缓存
在应用程序代码本身内部,您可以将数据缓存在内存中(即存储在变量中),以节省不必要的 API 请求或重复和复杂计算的成本。例如,如果您使用Redux管理应用程序状态,您可以使用reselect库来记忆推导的状态并防止不必要的重新计算。请按照以下步骤审核您的应用程序级缓存配置:

  1. 查找应用程序的计算量大或 API 请求量大的部分 
  2. 离开这些页面,然后再返回
  3. 如果为 API 请求配置了缓存,请检查开发工具中的网络选项卡以查看是否重复了任何额外的 API 请求
  4. 如果缓存配置为重计算,使用控制台日志查看是否有任何不必要的函数调用

在 DoorDash,我们使用GraphQLapollo-client ,它们支持具有多种[url=https://www.apollographql.com/docs/react/data/queries/supported-fetch-policies]缓存策略[/url]的查询(即 API 请求)缓存。在我们的审计过程中,我们浏览了几个主要查询的缓存策略,以查看应用的策略是否证明其使用是合理的,并查看缓存内部以确认预期数据是否存在正确的缓存键。
因为一切都按预期工作,所以我们没有进行任何更改,但这是我们审核现有缓存设置的好机会。

结论
DoorDash 的网络性能调查产生了许多想法和举措,但其中大部分都被证明是死胡同。将性能技术应用于大型代码库是具有挑战性的,并且在没有跨团队彻底协调和沟通的情况下为高流量网页执行此技术也是有风险的。尽管如此,我们还是发现了一些可以快速应用的方法,并从该过程中吸取了有用的经验教训,其中包括:

  • 管理 Javascript 包大小:检查包大小的依赖关系并使用诸如摇树之类的功能进行优化。
  • 推迟在视口外渲染 DOM 元素:为了减少执行时间,只在视口内渲染必要的组件,并确保优化不会恶化 SEO。 
  • 通过减少 API 瓶颈来提高渲染时间:在审查 Web 性能时还要测量 API 响应时间,因为它可能是瓶颈之一。
  • 延迟加载渲染阻塞资源:为了限制渲染阻塞,异步加载非关键资源并防止长时间运行的脚本。
  • 应用正确的缓存策略:确保缓存的每一层都正确配置了正确的缓存策略。

我们希望我们的发现为工程师审核 Web 性能和应用此处概述的技术以提高其站点性能提供起点。

详细点击标题