an ckick pictrue

吃饭不洗...segmentfault 专栏

6s到1s: 双端应用的秒开优化之路

Issue链接更新于:2023-02-27

背景

近来,我们的业务开始尝试用新的解决方案(双端开发,同时投放PC端和移动端)解决前端资源短缺问题。但提测后,测试提出我们移动端页面首屏太慢(6s+),体验极差。

我尝试在PC端打开这个页面,耗时4s+,加载瀑布流长下面这样:

20230227202023

粗略一分析,存在下面三个问题:

  • 应用页面采用了懒加载策略,详见红色圈注区域;
  • 入口js,包体积太大,gzip之后还有1.3M, 原始大小4M+
  • 日常静态资源未开启cdn

优化三部曲

一、应用架构优化

在应用架构中,懒加载是一个常见的优化技巧,它可以在需要时才加载资源,减少应用的启动时间和流量消耗。但是,懒加载并不是万能的,有时候过多的懒加载反而会降低应用性能。因此,在进行应用架构优化时,应该根据具体情况权衡是否采用懒加载。

20230227204832

根据瀑布流结合上图的分析,资源的加载用时4s+,index.html 的时间对于前端来说是不可控的,这完全取决于域名的解析和服务器的响应。但js 和 css 这一段是完全可控的,我们可以把后面 500ms 的资源加载用时提前,也就是去除懒加载的架构方案。

二、包体积优化

包体积优化是优化应用性能的重要一环。包体积的大小直接影响应用的加载时间和用户体验。

当看到1.3M的gzip包大小后,我们第一反应就是去开启webpack的analyse查看包体积分布,结果完全超出想象(这是development模式,注释了其他页面入口,仅保留了时段菜单一个页面): 20230227205657

上面这张图也很直观的暴露出几个问题:

  • 同一个chunk同一个依赖被多次打入(以图中的cook-design为首,见下图)
  • 包的大小分布极不均匀
  • 不同chunk打入同一版本依赖(懒加载策略造成)

项目依赖管理,防止多版本

20230227210640

在项目依赖管理中,应该尽可能避免多版本的依赖关系。多版本的依赖关系会导致包体积变大,甚至会产生冲突和错误。在日常业务组件开发中,对基础组件(比如antd)的依赖,可以设置为peerDependencies。

我们的解决思路是通过规范业务的依赖引入方式 和 通过resolution 强制指定版本。

组件库支持treeShaking

在使用第三方组件库时,通过treeShaking,可以只加载应用中所需的组件和函数,减少包体积和加载时间。以往我们引入antd3时,为了防止包体积过大,就会引入babel-plugin-import帮助我们去除无用代码。但antd4遵从es-module规范,并配置了sideEffect,使得天然支持treeShaking。

我们的项目大量依赖了本地生活的 cook-design 与 cook-design/icons,这两个由于缺少 treeShaking 配置,导致组件被完整打了进来,其冗余组件与冗余组件带来的依赖,体积在5M左右。

所以我们在和相关的维护者交流了相关想法后,其同意加配置升级支持treeShaking,我们团队自己的双端组件库cook-design-mixin也支持了这个配置。

在优化应用架构、优化依赖的版本、组件支持treeshaking、以及采用分包策略后,我们的构建结果(production 模式)长下面这样:

20230227214215

去除无用代码

将应用中的无用代码去除是包体积优化的重要手段之一,我在文章开头提到,我们这个项目是一个双端架构的项目, 它是一个编译时方案,这就是意味着pc端和移动端投放是两套不同的资源。但由于我们是在一个应用中开发,所以pc端的代码和移动端的代码是混着写的,我们通过规范和构建插件来解决代码混写的问题。

// 下面是一个示例
import { Decorator } from '@alife/wa-mixin-core';
import DMobile from './wa-mobile';
import DDesktop from './wa-desktop';

export default (props: {
  isShowTipStatus: boolean;
  showMode?: boolean;
}) => {
  return Decorator({
    Mobile: <DMobile {...props} />,
    Desktop: <DDesktop />,
  });
};

但通过代码分析,我们发现下面两个问题:

  • 双端组件库早期编写的组件存在大量的编写不规范,导致构建插件无法shaking掉另一端的代码,从而导致移动端打入了pc端的代码(涉及面太广,后续做专项优化)
  • 业务代码由于是存量业务(这里指菜品下发)转双端(前期pc端代码已开发上线,移动端采用双端方案开发),所以导出的不规范,导致包体积剧增

当我们把菜品下发入口加入时,构建结果长下面这样,比上面单入口的增加2.3M(压缩后的): 20230227214028

而只留下发一个入口,构建结果长下面这样:

20230227214606

下面页面的引入,导致大量的cook-design 与 cook-design/icons被引入,以致于我当时在考虑要不要为icon单独拆一个包或者通过cdn引入。

包入口被治理后,构建包大小明显达到了改善: 20230227215027

gzip 之后,大小基本在1M以类(还有改善的空间):

20230227215141

三、网络请求优化

网络请求优化是优化应用性能的另一个重要环节。在进行网络请求优化时,可以考虑以下几个方面:

利用combo降低请求数量

在前端应用中,通常需要加载多个JS、CSS、图片等资源文件,这会产生大量的网络请求,影响应用性能。可以使用combo技术,将多个文件合并成一个文件请求,减少网络请求的数量, 不过这也需要服务器的支持。我们的应用模版将react、moment、babel-polyfill等使用cdn作为外部依赖引入,但为了减少请求数,使用了combo服务。

利用http2的多路复用提供请求效率

http2支持多路复用,可以在一个连接上同时传输多个请求和响应,提高请求效率和传输速度。因此,在使用http2协议时,可以利用多路复用优化网络请求。所以才有了上面分多个包的策略,为了能最大化利用现代网络的带宽。

等待队列优化

20230227220019

在分包依赖完成后,我们发现js资源包的加载存在明显的等待情况(如上图),而且是稳定复现的,经过分析,我们发现脚手架提供的应用html模版,存在同步script引入vconsole,从而阻塞了js资源的下载。

减少DNS解析 与 TCP的握手时间

在排查等待队列问题时,我们还发现,html模版中应用部分外部依赖使用了非集团的cdn域名问题。在http1时代,这个做法是被提倡的,因为浏览器对单个域名同时的http连接有限制,所以为了增加并行请求数,在同一个网页中,会采用几个cdn域名。但http2,这个就成了束缚,而且采用外部cdn,会带来安全风险。

在上面的策略全部实施后,日常环境,应用首屏稳定在1.5s左右。待双端组件库按规范调整后,这个数据能再快10%。

开启cdn加速

什么是cdn,相信大家都应该很了解了。在我们应用上线后,静态资源终于得到了cdn魔法的加持。页面在我们的投放app掌客端也顺利实现了秒开,体验极好: 20230227222820

总结

首屏优化是前端经久不衰的话题,其手段丰富、细节繁多。应用架构优化、包体积优化和网络请求优化是优化前端应用性能和提高用户体验的关键所在。

本文由ChatGPT拟初稿,打工人加工而成。 20230227223210