Uber 高性能 Web App 优化实践
原文 - Building m.uber: ENGINEERING A HIGH-PERFORMANCE WEB APP FOR THE GLOBAL MARKET
Performance matters on mobile.
又是一篇关于性能优化的实践。
m.uber 团队对 m.uber - 即他们的超级轻量 web app 做了一些性能优化的工作。
范围全面,从代码到打包到部署到缓存,都有涉及。
TL;DR
Performance Tools
- Preact over React
- Webpack dynamic bundle splitting & tree-shaking capabilities
- Tiny Libraries & Minimal Dependencies
- source-map-explorer
下面是正文:
Smaller, faster: how we built it
技术栈
代码层面:ES2015+,使用 Babel 编译;
框架:Preact
+ Webpack
;
设计的痛点是,在保证类 Native App 丰富体验的同时,最小化客户端体积。所以 Preact + Webpack 的 dynamic bundle splitting 和 tree-shaking capabilities 功能完美搭配。
Initial server render 首次服务端渲染
因为在所有核心打包的 JS 全部被下载前,客户端或浏览器都不能开始渲染 markup 标记。
为了更快首屏渲染,m.uber 在首次浏览器请求时候会返回服务端渲染好的 Preact,且 state
及 markup
都嵌到行内,全部都是字符串,所以这些内容一旦被 客户端下载,就可以立马加载出来。
Serve bundles on demand 按需打包加载
m.uber 中大部分 JS 都是用来做一些辅助功能,这些都是没有必要一次性加载的,所以他们用了 Webpack 的 Code Splitting
工具按需加载代码。
(这个我在 Accounts 项目也实践过,一个巨大的 JS 文件拆分成了三个小的 JS 文件,不过也需要考虑每个 HTTP 请求时间。)
We use a splitPage function that returns the ancillary bundle wrapped in an asynchronous component.
// Example: settings page
const AsyncSettings = splitPage(
{load: () => import('../../screens/settings')}
);
// 当且仅当 `AsyncSettings` 被 Parent Component render() 调用,
// setting bundle js 才会被下载.
Tiny Libraries 更小的库
m.uber 本意上是希望在 2G 网下也能飞快,所以打包后的体积也很重要。
他们 App 核心部分(叫车功能)在 gzipped & minified 后只有 50kB 大小,所以在典型 2G 网(50kB/s, 500ms 延时)下,也能三秒内开始交互。
以下是优化前后的打包和依赖的对比。
Preact over React
体积原因, Preact (3kB GZip/minified) < React (45kB).
Preact 除了不支持 PropTypes
和合成事件外,还是可以的。
Preact 据说在组件和元素回收可能有点点问题,不过他们还是正在解决的吧... 反正 uber 的人觉得他们用着还不错。
Minimal Dependencies 最小化依赖
为了对付前端打包一个明显的依赖膨胀(dependency bloat),Uber 的人对安装的 npm 包特别挑剔。
他们推荐使用 Just
那个包,或者参考它的 npm package 思想:一个函数只做一件事,且无其他任何依赖。(去 Just GitHub 看,包作者就是写这篇文章的人...)
与服务器端进行数据传输代价高昂,所以一些特别大的库,比如 Moment
这种超大的 库就是不需要被下载的(但是我在想的是,他们是自己实现了一套类似 Moment 的东西吗?)
推荐了 source-map-explorer 工具来分析加载的依赖。这样的话能很直观的知道以后从哪块开始进行代码层面的优化。
于是我用这个工具来分析了下自己的项目 creator-main-js
。
Conditional Feature Set
首次加载会调用 window.performance
API,然后基于结果来考虑在该设备上是加载还是隐藏交互地图。
相当于是渐进增强的思想吧。
Minimal render Calls
和 React 一样,Preact 每次 render
Virtual DOM 都是有代价的。
所以优化方法也和 React 一样,尽可能地多用 shouldComponentUpdate
最小化 render
的调用。
Caching
Service Workers
又见 service workers,截获 URL 请求,修改网络和本地磁盘资源获取变为配置好的获取逻辑。
Uber 把首次 HTML 响应和 JS 包都用 SW 缓存了,所以就算间歇性断网 m.uber 也还是能渲染内容。
SW 也极大程度上降低了加载时间。 磁盘 I/O 性能在不同的 OS 和设备上都很不一样,就算从磁盘缓存拿数据也是特别慢。SW 支持从浏览器缓存加载所有内容。
m.uber 客户端在每次 build 后都会安装一个新的 SW。
Webpack 每次都会生成动态打包名,build 环节会把新的打包名直接写到 service worker 模块里面。
一旦安装,会首先缓存核心 JS 库,然后 lazy-loading 缓存 HTML 及一些辅助的按需加载的 JS 包。
Local Storage
Uber 的人觉得每次把经常变化的响应数据缓存到 SW 不太好,他们就把这些都扔到了浏览器 localStorage
.
m.uber 每隔几秒就会从服务器端拉 ride status,再把这些最新的 status data 都放在 localStorage
中,当乘客返回 App 时候,就能立马重新渲染页面,而不需要等待 API 链路耗时。
每次的 status data 很小且体积有限,所以存储的更新很快,可依赖性也好。
他们最后终于意识到,其实并不需要类似 indexedDB
这样的本地异步存储 API。