[译] 咱们是怎样把 Carousell 的移动端 Web 体验搞快了 3 倍的?

咱们是怎样把 Carousell 的移动端 Web 体验搞快了 3 倍的?

回顾一下咱们构建 Progressive Web App 的 6 个月

Carousell 是一个在新加坡开发的移动分类广告市场,并在包括印度尼西亚、马来西亚和菲律宾在内的许多东南亚国家开展业务。咱们在今年年初为一批用户推出了咱们移动 Web 端的渐进式网页应用(PWA)] 版本。javascript

在本文中,咱们将分享 (1) 咱们想要创建更快的 Web 端体验的动机,(2) 咱们怎么完成它,(3) 它对咱们用户的影响,以及 (4) 是什么帮助了咱们快速完成。php

🖼 这个 PWA 在 mobile.carousell.com 🔎css

为何必定要有更快的 Web 体验?

咱们的应用是为新加坡市场开发的,咱们已经习惯于用户拥有高于平均水平的手机和高速的互联网。然而,随着咱们扩展到整个东南亚地区的更多国家,如印度尼西亚和菲律宾,咱们面临着提供一样使人愉快和快速的网络体验的挑战。缘由是,在这些地方,较通常的终端设备互联网速度 与咱们的应用设计标准相比,每每速度慢而且不太可靠。前端

咱们开始阅读更多有关性能的内容,并开始使用 Lighthouse 从新审视咱们的应用,咱们意识到 若是咱们想要在这些新的市场中成长咱们须要更快的 Web 体验若是咱们想要获取或是留住咱们的用户,那么一个网页在 3G 网络下(跟咱们同样)须要加载超过 15 秒就是不能接受的了。java

🌩 Lighthouse 的性能表现得分会是一个很好的叫醒服务~ 🏠react

Web 端一般是咱们的新用户发现和了解 Carousell 的入口。咱们想从一开始就给他们一个愉快的体验,由于 性能就是用户体验android

为此,咱们设计完成了一种全新的,性能优先的 Web 端体验。当咱们决定首先使用哪些页面作尝试时,咱们选择了产品列表页面和主页,由于 Google Analytics 的统计代表这些页面的天然流量最大。webpack


咱们怎么作到的

从现实世界中的性能预算开始

咱们作的第一件事就是起草性能预算,以免犯下未经检查的臃肿问题(咱们以前的 Web 应用中的一个问题)。ios

性能预算让每一个人都在同一个“页面”上。它们有助于创造一种共享热情的文化,以改善用户体验。具备预算的团队还能够更轻松地跟踪和绘制进度。这有助于支持那些拥有有意义的指标的执行发起人,指明正在进行的投入的合理性。git

你能负担得起吗?:现实世界中的网络性能预算.

因为 在加载过程当中存在多个时刻,都会影响到用户对这个页面是否“足够快”的感知,咱们将预算基于一套组合的指标。

加载网页就像一个有三个关键时刻的电影胶片。三个时刻分别是:它发生了吗?它有用吗?而后,它能用起来吗?

2018 年里 JavaScript 的花费

咱们决定为关键路径的资源设置 120 KB 的上限,在全部页面上还有一个 2 秒的 首屏内容渲染 和 5 秒的 可交互时间 限制。这些数字和指标都是基于 Alex Russell 的一篇发人深省的文章 真实世界的 Web 性能预算 以及 Google [以用户为中心的性能指标]。

关键路径资源          120KB
首屏内容渲染          2s
可交互时间            5s
Lighthouse 性能得分  > 85
复制代码

🔼 咱们的性能预算 🌟

为了能把性能预算坚持下去,咱们在一开始选择库时就十分慎重,包括 react、react-router、redux、redux-saga 和 unfetch

咱们还整合了 bundlesize 到咱们的 PR 流程当中,用来执行咱们在关键路径资源上的性能预算方案。

⚠️ bundlesize 阻止了一个超出预算的 PR 🚫

理想状况下,咱们也会自动检查 首屏渲染时间可交互时间 指标。可是,咱们目前尚未这样作,由于咱们想先发布初始页面。咱们认为咱们能够经过咱们的小团队规模来避免这种状况,每周经过咱们的 Lighthouse 审核咱们的发布,以确保咱们的变动在预算范围内。

在咱们积压的工做中,下一步就是自建性能监控框架。

咱们如何让它(看起来)变快了

  1. 咱们采用了一部分 PRPL 模式 咱们为每一个页面请求发送最少许的资源(使用 基于路由的代码拆分),并 使用 Workbox 预先缓存应用程序包的其他部分。咱们还拆分了没必要要的组件。例如,若是用户已登陆,则应用程序将不会加载登陆和注册组件。目前,咱们仍然在几个方面偏离了 PRPL 模式。首先,因为咱们没有时间从新设计的旧页面,该应用程序有多个应用程序外壳。其次,咱们尚未探索为不一样的浏览器生成单独的构建打包。

  2. 内联的 关键的 CSS 咱们使用 webpack 的 mini-css-extract-plugin 来提取并内联的方式引入对应页面的关键 CSS,以优化首屏渲染时间。这样就给用户提供了 一些事情 正在发生 的感受。

  3. 懒加载视口外的图像。 而且逐步加载它们。咱们建立了一个滚动观察组件,其基于 react-lazyload,它会监听 滚动事件,一旦计算出图像在视口内,就开始加载图像。

  4. 压缩全部的图像来减小在网络中传输的数据量。 这将在咱们的 CDN 提供商的 自动化图像压缩 服务中进行。若是你不使用 CDN,或者只是对图像的性能问题感到好奇,Addy Osmani 有一个 关于如何自动进行图像优化的指南

  5. 使用 Service Worker 来缓存网络请求。 这减小了数据不会常常变化的 API 的数据使用量,并改善了应用程序后续的访问加载时间。咱们找到了 The Offline Cookbook 来帮助咱们决定采用哪一种缓存策略。直到咱们有了多了应用外壳,Workbox 默认的 registerNavigationRoute 并不适用于咱们的实际场景,因此咱们不得补自行完成一个 handler 来匹配当前应用外壳的导航请求。

workbox.navigationPreload.enable();

// From https://hacks.mozilla.org/2016/10/offline-strategies-come-to-the-service-worker-cookbook/.
function fetchWithTimeout(request, timeoutSeconds) {
  return new Promise((resolve, reject) => {
    const timeoutID = setTimeout(reject, timeoutSeconds * 1000);
    fetch(request).then(response => {
      clearTimeout(timeoutID);
      resolve(response);
    }, reject);
  });
}

const networkTimeoutSeconds = 3;
const routes = [
  { name: "collection", path: "/categories/.*/?$" },
  { name: "home", path: "/$" },
  { name: "listing", path: "/p/.*\\d+/?$" },
  { name: "listingComments", path: "/p/.*\\d+/comments/?$" },
  { name: "listingPhotos", path: "/p/.*\\d+/photos/?$" },
];

for (const route of routes) {
  workbox.routing.registerRoute(
    new workbox.routing.NavigationRoute(
      ({ event }) => {
        return caches.open("app-shells").then(cache => {
          return cache.match(route.name).then(response => {
            return (response
              ? fetchWithTimeout(event.request, networkTimeoutSeconds)
              : fetch(event.request)
            )
              .then(networkResponse => {
                cache.put(route.name, networkResponse.clone());
                return networkResponse;
              })
              .catch(error => {
                return response;
              });
          });
        });
      },
      {
        whitelist: [new RegExp(route.path)],
      },
    ),
  );
}
复制代码

⚙️ 咱们对全部的应用外壳采用了一个超时时间为 3 秒的网络优先策略 🐚

在这些变化中,咱们严重依赖 Chrome 的“中端移动设备”模拟功能(即网络限制为 3G 速度),并建立了多个 Lighthouse 审计来评估咱们工做的影响。

结果:咱们怎么作到的

🎉 比较以前和以后的移动 Web 指标 🎉

咱们新的 PWA 列表页面的加载速度比咱们旧的列表页面 快 3 倍。在发布这一新页面以后,咱们的印度尼西亚的天然流量与咱们全部长时间的周相比,增加了 63%。在 3 周的时间内,咱们还看到,广告点击率 增长了 3 倍,在列表页面上发起聊天的匿名用户 增长了 46%

在较快的 3G 网络下的 Nexus 5 上,咱们列表页面的先后对比。更新:WebPageTest 对这个页面的简单报告。 ⏭


快速,自信地迭代

一致的 Carousell 设计系统

在咱们开展这项工做的同时,咱们的设计团队也在同时建立标准化设计系统。因为咱们的 PWA 是一个新项目,咱们有机会根据设计系统建立一组标准化的 UI 组件和 CSS 常量。

拥有一致的设计使咱们可以快速迭代。每一个 UI 组件咱们只构建一次,而后在多个地方复用它。例如,咱们有一个 ListingCardList 组件,它显示列表卡片的提要并触发回调,以便在滚动到结尾时提示其父组件加载更多列表。咱们在主页,列表页面,搜索页面和我的信息页面中使用了它。

咱们还与设计师合做,一块儿肯定应用程序设计中的适当性能权衡。这使咱们可以维持咱们的性能预算,改变一些旧设计以符合新设计,而且,若是它们太昂贵了的话,就放弃花哨的动画。

与 Flow 同行

咱们选择将 Flow 类型定义做为咱们全部文件的必选项,由于咱们想减小烦人的空值或类型问题(我也是渐进类型的忠实粉丝,但为何咱们选择了 Flow 而不是 TypeScript 就是下一次的一个话题了)。

在咱们开发和建立了更多代码时,采用了 Flow 的选择被证实很是有用。它让咱们有信心添加或更改代码,将核心代码重构得更加简单和安全。这使咱们可以快速迭代而不会破坏事物。

此外,Flow 类型也对咱们的 API 约定和共享库组件的文档很是有用。

对于强制将 Redux 操做和 React 组件的类型写出来这件事情,还有一个额外的好处,就是它会帮助咱们仔细思考如何设计咱们的 API。它也提供了与团队开始早期的 PR 讨论的简单途径。


小结

咱们建立了一个轻量级的 PWA 来为咱们具备不可靠网速的用户提供服务,一个页面接一个页面地发布,提升了咱们的商业指标用户体验。

是什么帮助咱们保持足够快的速度

  • 拥有并坚持一份性能预算
  • 下降关键渲染路径到最小
  • 常用 Lighthouse 进行审计

是什么帮助咱们快速迭代

  • 拥有标准化的设计系统及其相应的 UI 组件库
  • 拥有彻底类型化的代码库

结束思考

回顾过去两个季度咱们所作的事情,咱们为咱们新的移动 Web 业务体验感到无比自豪,咱们正在努力使其变得更好。这是咱们第一个专一于速度的平台,也更多的思考了一个页面的加载过程。咱们的 PWA 对业务和用户指标的改进有助于说服公司内部更多人去了解应用程序性能和加载时间的重要性。

咱们但愿本文可以启发您在设计和构建 Web 体验时考虑性能。

在此为参与这个项目的人欢呼:Trong Nhan Bui、Hui Yi Chia、Diona Lin、Yi Jun Tao 和 Marvin Chin。固然也要感谢 Google,特别是要感谢 Swetha and Minh 对这个项目的建议。

感谢 Bui、Danielle JoyHui YiJingwen ChenSee YishuYao Hui Chua 的写做和校对。

最后,多亏了 Hui YiYao Hui ChuaDanielle JoyJingwen ChenSee Yishu

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索