前端性能优化方法与实战 - 前 58 集团技术副总监 - 拉勾教育

上一讲我介绍了图片骨架屏和 SSR 这两种优化方案,它主要聚焦于白屏优化,接下来这一讲我将介绍 WebView 层面的优化和前端架构性能方面的调优。

为什么是这两个方面呢?首先 WebView 是我们经常使用到的工具,在我们开发 App 过程中起到非常重要的载体作用,甚至我还曾见过专门招聘 WebView 开发工程师的信息。而前端架构的某些因素也会严重影响我们的前端性能体验,所以,这一讲我就重点来介绍下它们。

WebView 性能优化

WebView 是一个基于 WebKit 引擎、展现 Web 页面的控件, App 打开 WebView 的第一步不是请求连接,而是启动浏览器内核。这意味着,在浏览器端,我们输入地址就开始请求加载页面,但在 App 内,我们还需要先初始化 WebView 然后才能请求和加载。

这会造成什么结果呢?同一个页面,在 App 端外反而比端内打开速度更快。因为在 App 内,WebView 还需要先进行初始化,这需要时间,且这个初始化时间还和 WebView 类型有关。其中 Android 下只有一个 WebView,而 iOS 下却分 UIWebView 和 WKWebView。以我们 iOS 端使用的 UIWebView 为例,需要 400ms 左右,如果是 WKWebView,时间会更短,但基本也会占首屏时间的 30% 左右。

怎么解决这个问题呢?这就需要进行 WebView 优化了, 一般它的优化包括资源缓存、并行初始化、资源预加载和数据接口请求优化,以及更换 WebView 内核等。

其中缓存选用方面比较简单,直接选用的浏览器默认缓存。而更换 WebView 内核,往往会因为需要进行灰度处理,必须一段时间内(通常几个月)并行两套 WebView 方案,很容易出现系统性风险,比如修改一个严重 Bug 后,前端工程师不知道用户端什么时候生效。所以,在这里,我着重介绍下 WebView 优化里面的并行初始化、资源预加载、数据接口请求优化三个方案。

并行初始化

所谓并行初始化,是指用户在进入 App 时,系统就创建 WebView 和加载模板,这样 WebView 初始化和 App 启动就可以并行进行了,这大大减少了用户等待时间。

如果是使用 native 开发的应用,根据用户在首页的访问路径,选择初始化策略,操作体验会更好。以携程 App 为例,假设用户进入首页后,停留在西双版纳自由行区域,直接加载 WebView 和模板,两者同时运行,此时首屏主要工作就变成加载接口请求数据和渲染模板部分的工作了。

为了减少 WebView 再次初始化的时间,我们可以在使用完成后不进行注销,将里面数据清空,放进 WebView 池子里面,下次使用时,直接拿过来注入数据使用即可。注意,使用时,要对 WebView 池子进行容量限制,避免出现内存问题。

另外还需注意一点,由于初始化过程本身就需要时间,我们如果直接把它放到 UI 线程,会导致打开页面卡死甚至 ANR(Application Not Responding,应用无响应),所以,我建议将初始化过程放到子线程中,初始化结束后才添加到 View 树中。

资源预加载

资源预加载,是指提前在初始化的 WebView 里面放置一个静态资源列表,后续加载东西时,由于这部分资源已经被强缓存了,页面显示速度会更快。那么,要预加载的静态资源一般可以放哪些呢?

  • 一定时间内(如 1 周)不变的外链;
  • 一些基础框架,多端适配的 JS(如 adapter.js),性能统计的 JS(如 perf.js)或者第三方库(如 vue.js);
  • 基础布局的 CSS 如 base.css。

一般在 App 启动时,系统就加载一个带有通用资源模版的 HTML 页面,虽然这些静态资源不经常变化,但如果变化呢?怎么避免因变化导致 App 频繁发布版本的麻烦呢?

一个办法是通过静态资源预加载后台进行管理。具体的话,我们不需要从 0 到 1 搭建,只需要在离线包后台添加一个栏目即可。

在业务接入预加载功能时,前端工程师通过静态资源预加载后台发布出一个静态资源列表页,然后把它的 URL 提供给 App,App 启动时会对这个 URL 下页面中的静态资源进行预加载。之后,前端工程师就可以查看静态资源的编号 ID、URL 和类型,进行删除、添加等管理操作。

不要小看这一点,通过这种做法,我们手机列表页 13 个文件缓存后,首屏时间从 1050ms 降低到了 900ms。

数据接口请求优化

数据接口请求优化,主要是通过同域名策略和客户端代理数据请求来实现。

其中,同域名策略是指前端页面和资源加载,尽量和 App 使用的数据接口在同一个域名下,这样域名对应的 DNS 解析出来的 IP,由于已经在系统级别上被缓存过了,大大降低了加载时间。

比如,58 App 客户端请求域名主要集中在 api.58.com,请求完这个地址后,DNS 将会被系统缓存,而前端资源的请求地址在 i.58.com,打开 WebView 后,由于请求了不同的地址,还需要重新去 DNS 服务器去查询 i.58.com 对应的 IP,而如果前端也改到 api.58.com 后,DNS 查询的时间可以从原来的将近 80ms 降低到几 ms。

客户端代理数据请求,则是指把前端的数据请求拦截起来,通过客户端去发送数据请求。因为正常的页面加载顺序是,前端在 HTML,CSS,JS 拉取下来之后才开始由 JS 发起前端的 ajax 请求,获取到数据后程序才开始进行填充。而我们通过客户端代理数据请求,可以把前端的 ajax 请求提前到与页面加载同时进行,由客户端请求数据,等 H5 加载完毕,直接向客户端索要即可。如此一来,便缩短了总体的页面加载时间。

注意,这里的数据拦截环节,Android 端可以重写 WebViewClient 的 shouldInterceptRequest 方法,iOS 端没有类似的方法,只能通过私有 API 方案、自定义协议方案和 LocalWebServer 来实现

前端架构性能调优

前端架构性能优化,是指通过在前端开发、编译、打包发布环节所作的优化,以此来提升前端性能的方案。因为我们比较关注首屏时间,对这方面贡献比较大的是开发和打包发布这两个环节,所以接下来我着重介绍下 Vue 开发过程中的长列表性能优化和 webpack 打包分析层面的优化。

长列表性能优化

一般,Vue 会借助 Object.defineProperty 这个 ES5 规范的方法,对数据进行劫持,即通过在某个对象上定义一个新属性或者修改一个属性,实现视图响应数据的变化。

这会造成什么影响呢?

在一些纯展示的场景里面,比如电商列表页面,如果还允许 Vue 劫持我们的数据,会花费很多的组件初始化时间。这种情况下,怎么做呢?可以使用 Object.freeze 冻结这个对象从而避免修改。

  1. export default {
  2. data: () => ({ goodsList: [] }),
  3. async created() {
  4. const goodsList = await this.$service.get("/getGoodsList");
  5. this.goodsList = Object.freeze(goodsList);
  6. }
  7. };

以前面提到的列表页面优化为例。我先定义一个 goodsList 的空对象,通过 async 将 created 钩子函数的返回值(也就是一个商品列表)封装成一个异步 Promise 对象,然后在 created 钩子函数中向 getGoodsList 接口获取数据。

其中,Vue 的生命周期里对外暴露的 created 钩子,表示 Vue 实例被创建但还没有渲染到浏览器的阶段;await 表示当拿到返回的数据结果后,Vue 实例才会通过 Object.freeze 把 goodsList 结果冻结,即 goodsList 对象展示过程中,数据变化时,视图将不再更新。

通过以上步骤,最终提升商品列表页的性能。

打包优化

打包优化方面,我们可以通过 webpack 插件来完成。 wepack 输出的代码可读性较差,而且文件比较大,我们很难了解打包后的情况,更别说如何优化了。为了直观分析打包结果,我们可以使用一个 webpack 插件——webpack-bundle-analyzer,通过它可以对打包结果进行可视化分析。

具体怎么实现呢?

我们在 wepack 中加入以下代码来实现打包分析。

  1. module.exports = {
  2. chainWebpack (config) {
  3. if (process.env.NODE_ENV === 'production') {
  4. config.plugin('webpack-bundle-analyzer')
  5. .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
  6. .end()
  7. }
  8. }
  9. }

这里面 chainWebpack 是 vue3 提供的一种高级操作——链式操作,通过它可以快捷完成一些方法的调用。代码中添加 process.env.NODE_ENV === ‘production’是为了让代码只在打包时起作用。
做完以上操作后,运行 npm run build —report,即可后生成一个分析报告。

15 | 高级进阶:WebView 层及代码架构层面优化 - 图1

当我们拿着鼠标在上面滑动时,就可以看到整个包的组成部分,以及每部分的信息。一般我们可以找内容比较大的方面,然后分析原因进行优化。

比如,有次游戏业务发现打包目录超过了 10MB,仔细定位发现是有些 game.map 的文件打包上来了。虽然 game.map 文件便于我们开发时调解 bug,准确定位错误的位置,但在这里却影响了我们的性能体验。找到原因后,解决它也很简单了,直接在打包时,关闭 sourcemap,即在配置文件中增加 productionSourceMap:false 就可以了。

小结

好了,以上就是 WebView 性能优化和代码架构层的优化,这里面有一些注意事项。WebView 会占用一定的内存,如果使用 WebView 缓存池进行优化,会出现内存占用多的问题,我们可以将 WebView 放到独立进程中,避免内存泄漏。当然,WebView 独立进程的话,就需要解决进程间调用问题,一般可以直接使用 Aidl 来解决。

下面给你留一个问题:

目前你一般对 WebView 进行哪些方面的优化?

欢迎在评论区和我沟通,下一讲我将介绍预请求、预加载及预渲染机制方面的内容。