海阔天空的云

我们在自己的世界里独自狂欢

0%

2020第二季度前端性能优化

前提

我是喜欢做一些前端优化相关的工作的,最近又做了一些,也踩了一些坑。于是就分享记录一下。

之前遇到的问题

之前遇到的显著问题是由于历史债,或者说为了省事,在做一些webpack相关的流程的时候,采用了在入口处将所有资源都打出去的做法。然后将一些体积比较大的npm包,或者用处比较多的npm包,通过webpack的external方式来分离出去,通过这样的操作,让主入口的index.js文件尽可能地小。

这样的做法有什么优缺点呢?

优点:

只需要一个index.js文件,其他的资源都可以通过external的方式获取。

用户从着陆页跳转到其他的页面的时候,只要是在SPA之下,就无需再请求webpack打包后的其他资源,这样地话,打开这些页面会很快,因为资源早就预先加载过了。

缺点:

由于将整个项目中用到的所有资源都打包到了入口中(除了external的资源),致使有一些页面,尽管并不会被用户访问,也已经直接在打包的文件中了。那必然导致,这个index.js 体积很大,那么如何解决这个问题呢?

举一个形象的例子,当时的做法就是费了很大的力气,我用卡车拉了一车的水果到你家门口,西瓜,苹果,大鸭梨,应有尽有。你想要什么水果,我能够立刻就把你要的水果给到你。但是,我可能只是想要两个苹果,你却要为此浪费那么多汽油。

我的方案,其实是一个通用的方案,也就是React 官方文档推荐的code-splitting 的方式,用到了Suspense 以及Lazy 这两个方法来实现。

其他的,也就是Webpack 4中配置dynamic-imports 的方法了。

这里需要解释的是,其实我早在上家公司的时候,就已经使用这样的方式来进行code spliting。 之所以这次又做了这方面的工作,也是因为当前公司项目的一些技术债。

一个项目,多个项目

我来这家公司之前,对微前端只停留在认知层面,并没有太多了解。后来,来到这家公司,慢慢地发现原来还有我们这样的项目。

产品究竟是什么

产品是SAAS的,面向企业的软件。它的最终目的是希望能够帮助企业在遇到危机或者突发事件的时候,合理地调配,处理一些事情。比如飓风来袭之前,向飓风所在地的员工发送短信,邮件提醒,管理员需要能够快速执行一个预案,这个预案也可以说是一个任务列表,逐个任务去完成,直到所有任务完成,它的工作才算完成,这所有的一切都是这个软件提供的。

然而虽然大体上说是一个软件,但是更具体地来说,应该是多端的服务。

首先是需要一个管理终端,这个终端能够去为所有SAAS用户设置相应的权限,依据它们所购买的产品。这个管理终端,我们可以跟它叫做admin portal.

另外,还需要一个SAAS用户的管理终端,这个终端可以依据你购买的产品,提供相应的服务。这些服务,包括发送消息,包括发布任务等等。这个SAAS用户的管理终端,我们可以跟它叫做manager portal.

其次,还需要一个普通用户终端,这个终端接受来自manager portal的指令,例如下达的任务等。我们可以跟它叫做member portal.

我们现在搞清楚了,这样一个大的SAAS产品,需要多端来进行支持。

如果你还不明白,也可以有个更加通俗易懂的解释。我们普通用户逛的淘宝,其实是淘宝买家版,淘宝的卖家也需要一个终端来管理和维护商品,这就是淘宝卖家版。除此之外,还有一个淘宝的员工才能用的终端,通过它可以看到所有的卖家数据,可以在这个终端上面,设置一些全场优惠券。所以,我们可以看到,一个大的产品,下面相应地会有多端产品。

而即便是一个淘宝买家版,也会根据用户使用设备不同而分成不同版本,比如PC网页版,手机网页版,安卓手机APP版,苹果手机APP版,Ipad版本等等,所以一个产品需要维护的内容实际上是相当多的。

好了,我们现在不谈淘宝了,来继续说我们的SAAS产品。

现在为了说明清楚,而又不泄露一些公司的隐私。我们假设我们做的产品是一个类似于微盟或者有赞这样的一个产品,它可以给一些中小商家提供搭建电商店铺的能力。我们可以暂时给它起一个名字,就叫做微赞 。好了,现在我们继续来分析我们的微赞。

微赞有一个admin portal,它用来管理商户。还有一个manager portal, 它是卖家的平台,方便卖家上下架商品,做一些促销活动,查看最新的销售数据等等。当然,微赞还有一个member portal,它是消费者终端,可能是小程序,也可能是微信网页,还可能是APP,最终都是要让消费者消费的入口。

然而,如果微赞细分开来,又可以分成多个不同的模块。比如优惠券模块,又比如百亿补贴模块,这个时候,我们当然希望我们地微赞更稳定,这里的稳定从以下几个方面来谈:

  1. 各个功能模块,可以单独部署,这样每一个新的功能开发完成,都不会等待其他的模块,而可以直接上线。

  2. 各个功能模块之间应该相互独立,这样能更好地支撑第一点。项目也能足够简洁。

所以,这个时候,我们发现针对第一个问题,我们需要引入微服务的概念,这个时候,对于后端服务来说,每个服务就能够单独部署,上线。相对应的,前端要做好支持,也需要引入微前端的概念,这样的话,才好。

code spliting 又多种方式,在webpack的官方文档中,提到了两种比较推荐的方式。第一种方式,是我们以前就用到的webpack打包方式,其原理说白了,就是把那些external的资源拿出去,把那些common的资源也都拿出去,单独打包,对于这个入口来说,还是只有一个chunk。页面在初始化的时候,就已经把所有的资源全部加载上了,之后,你就用就好了。第二种方式,就是我们现在用的这种方式了。

我做的工作厉害吗?并不厉害,事实上,这个工作只需要按照文档按部就班就可以完成。

但是接下来,还是有一些不一样的东西的。

混合应用

前面说了,我们是想将我们的项目,进行微前端化处理的

这个时候,不光用到了动态加载,也在恰当的时机,用到了loadRemote , 这是我们自己写的一个加载远程模块的机制,通过它,我们可以实现类似node imort, require那样的效果。

之所以这样做的目的,是有一下几点。

第一,前面说了,我们会有多端产品,而这多端产品中,又有大量的业务组件是重复的,比如一个表单组件,这个表单组件并不单单是一个UI组件,也是我们自己定制好的一个业务组件,跟业务绑定的非常紧,随着开发和迭代,这个表单组件,可能每周都会有代码的提交,这个时候,我们希望对这个表单组件进行一个有效的管理。但是受限于基础设施,我们当时还并没有一个私有的npm库来完成这样一个工作,所以我当时就做了一个工作,通过loadRemote的方式,来把这个组件单独build出来,这样的话,这个组件也就能够被另外一端的产品访问了。不过值得一提的是,这个方案乍看起来还很不错,但是遇到了一个经典的问题,那就是跨域。这里可以详细说一说,比如我在manager 端对这个组件进行build,最后生成的域名是manager.test.com 而我想要在member 端引用这个组件,那么就会引起跨域的问题。解决这个问题,也很意外,非常凑巧的是,我们的后端在使用K8S的时候,就将两端的域做了整合。这样的话,如果访问member.test.com/mgr-static/abc ,它实际上会往manager.test.com/static/abc这个地址去解析。也就完美解决了跨域的问题。

第二,我们也发现,虽然用到了动态加载,但是由于有一些npm模块写得并不符合规范,导致出现了在多个chunk中某个npm包多次被打包进去的情况,这个时候,如果我们想要对打包后的chunk文件进行优化,一个方法就是将那些写得并不规范的npm包通过loadRemote的方式,单独打包出去,这样的话,它在最终打包的文件中,只会有一份结果,而不会像刚才我们所说的那样,在多个chunk中都有它的身影了。

事实上,关于这里提到的第一点,随着私有npm库的搭建,也许更加优雅的方式已经出现。关于这里提到的第二点,我也一直在找寻更加优雅的解决办法。

完稿时间: 20200917