随着优步向新市场的扩张,我们希望所有用户都能快速要求乘车,而不考虑位置、网络速度和设备。考虑到这一点,我们重新构建了web客户端,将其作为原生手机应用的可行选择。
与所有现代浏览器兼容,m.uber为低端设备上的骑士提供类似的应用程序,包括我们的本机客户端不支持的骑士。该应用程序也很小 - 核心ride请求应用程序仅在50kb中进入,使应用程序即使在2g网络上也能够快速加载。
在本文中,我们描述了我们如何构建M.UBER(发音MOO-BER)并探索在超级轻量级Web应用程序中实现原生应用体验的挑战。
更小,更快:我们如何建造它
M.UBER是用ES2015 +写的,使用巴别塔对于ES5 rancedilation。总体设计挑战是最大限度地减少客户占用空间,同时保持原生应用的丰富经验。所以,虽然我们的传统建筑利用反应(与回来的),browserify.对于模块绑定,我们进行了交换罚款因为它的规模,好处和Webpack因为它动态包分割和tree-shaking功能。下面,我们将讨论如何解决这些问题以及应用程序体系结构中的其他挑战:
初始服务器呈现
客户端在下载完所有核心JavaScript包后才能开始呈现标记,所以m.uber通过在服务器上呈现Preact来响应最初的浏览器请求。结果的状态和标记作为字符串内联在服务器响应中,这样内容几乎可以立即加载。
按需提供捆绑服务
M.UBER的目标是让用户尽快请求乘车,但我们的大部分JavaScript用于辅助任务:更新付款选项,检查旅行进度或编辑设置。为确保我们仅服务我们需要的JavaScript,我们使用WebPack进行代码拆分。
我们使用A.splitPage返回包裹在异步组件中的辅助套件的函数。例如,设置页面由以下函数调用:
const AsyncSettings = splitPage(
{load: () => import(' ../../screens/settings ')}
);
使用此函数,只有在何时何时获取设置捆绑包AsyncSettings有条件地包括父母使成为方法。对于非常慢的连接,AsyncSettings将呈现一个“加载”模式,等待bundle获取完成。
小库
即使在2G网络上,m.uber也被设计得很快,因此客户规模至关重要。我们的核心应用程序(应用程序的基本部分,允许您请求乘车)只有50kB gzip和缩小,这意味着3秒的时间来互动的典型2G (250kB/
缩写反应
在尺寸方面,我们选择Preact (3kB GZip/minified)而不是React (45kB)。Preact几乎可以做React所做的所有事情(它不支持)概括或合成事件)并增加了自己的一些漂亮的反射功能。谈到回收元件和元素时,缩写是一点过度的但他们正在努力),这意味着您可能必须在元素上定义键没想到,但否则它为我们的需求也很好。
最小的依赖关系
要使依赖性膨胀,我们是在客户中使用的NPM软件包选择性,利用图书馆只是它的模块只负责一个函数,没有依赖关系。我们发现将昂贵的数据转换限制在服务器上是有意义的,这样更重的模块就像片刻不需要下载。要识别依赖浮点的来源,我们致力于使用像这样的工具source-map-explorer。
有条件的特性集
m.uber的使命是在设备和网络允许的情况下,让任何地方的每个人都能轻松地要求乘车,并提供额外的功能。方法检测第一次交互的时间window.performanceAPI.并根据结果隐藏或加载交互式地图体验。对于我们无法检测到网络性能的用户,还可以在设置页面中开启或关闭映射。
最小的使成为调用
预作用(如反应)在发生更改时使用VDOM生成新的标记,但这并不意味着调用使成为是免费的。它需要大量的JavaScript对话使成为想知道什么都不需要发生。我们使用shouldComponentUpdate广泛地减少对使成为。
缓存
服务工作人员
Service worker拦截URL请求,使网络和本地磁盘获取被自定义获取逻辑所取代,这通常利用浏览器的缓存API。通过缓存最初的HTML响应和JavaScript包,service worker允许m.uber在间歇性网络丢失的情况下继续提供内容。
Service worker还可以显著减少负载时间。磁盘I/O性能在不同的操作系统和设备之间差异很大,在许多情况下,甚至从磁盘缓存中获取数据也是如此慢得令人沮丧。在支持service worker的地方,所有重新获取的内容(包括HTML)都直接来自浏览器缓存,从而使页面能够立即重新加载。
M.uber客户端每次构建后都会安装一个新的service worker。因为WebPack会生成动态的bundle名称,所以我们的构建过程会直接向service worker模块写入新的名称。在安装时,我们缓存我们的核心JavaScript库懒洋洋地在获取HTML和辅助JavaScript包时缓存它们。
本地存储
当我们需要缓存对于service worker来说太不稳定的响应数据时,我们将其保存到浏览器的本地存储中。M.uber每隔几秒钟就会对乘客的乘坐状态进行一次调查;在本地存储中保存最新的状态数据意味着当骑手返回到应用程序时,我们可以快速地重新呈现他们的页面,而无需等待往返于API。由于我们的状态数据很小,存储数据的大小是有限的,存储更新是快速和可靠的,我们最终发现我们不需要使用异步本地存储APIindexedDB。
样式
Styletron
样式被定义为每个组件中的JavaScript对象。当组件被呈现时,Styletron从这些定义动态生成样式表。样式与组件的Colocation允许轻松拆分bundle和异步加载样式。没有使用的CSS永远不会被加载。
STYLETRON通过为每个唯一规则创建原子样式表来删除样式声明,允许最小的CSS运行时和一流的渲染性能。我们使用Styletron来生成m.uber上的所有组件级CSS。
svg
为了节省空间,我们使用SVG格式的图标类图像,并将其内联在使成为方法。为了调优,我们使用SVGO再加上手动优化,进一步缩短路径。有时,我们可以用基本形状来替换折线,并且使用带有合适除数的视图框尺寸来避免路径中昂贵的小数。
这一策略对整体应用规模的影响非常显著;例如,我们将logo的大小从7.4kB (png)减少到500字节(调整SVG)。
字体
通过明智地使用大小和颜色,我们发现我们能够完全消除自定义字体,而不会显著影响视觉设计。
错误处理
一个精益技术栈并不总是有助于简单的错误诊断,所以我们添加了一些轻量级工具来帮助,例如:
- 我们没有使用大量现成的错误监视库,而是进行了扩展window.onerror将错误发布到服务器上的客户端错误报告程序。
- 我们通过包装Preact的方法来避免递归的生命周期方法错误使成为和shouldComponentUpdate。
- 在我们的设计中,CDN托管文件抛出的错误不会提供有用的数据window.onerror除非跨源资源共享(CORS)头提供了适当的权限。然而,即使有这样的头,在异步事件期间抛出的错误也不能追溯到父模块,因此window.onerror将留在黑暗中。我们包裹了所有事件侦听器以允许错误通过Try / Catch将错误传递给父模块。
下一个步骤
通过我们在m.uber上的工作,我们已经花了很多精力在一个性能包中创建一个本地的、类似应用程序的体验,但我们还没有完成——还有很多改进的机会。在接下来的几个月里,我们计划发布更多的优化,包括:
- 通过让组件只接受原语和数组道具的平面集合,形式化最小化呈现调用的策略。这将允许我们使用React.pureComponent(自动实现shouldComponentUpdate具有浅的道具比较)和使成为专注于标记生成,而不是分支逻辑和其他切线任务。将API响应转换为扁平的原语可以委托给服务器逻辑(参见normalizr)和/或mapStateToProps作为适当的。
- 将actions和reducers结合起来,这将使bundle分离更加直观。
- 使用http / 2并将轮询api替换为Push通知。
此外,我们正在将m.uber的基础架构部分抽象成一个开源架构,这将成为未来轻量级Uber web应用的基础——请关注关于这个主题的后续文章。
图片标题:“分支上的高性能猎豹”,作者Conor Myhrvold,奥卡万戈三角洲,博茨瓦纳。





