4

前言

不知不觉又马上过年了,突然意识到今年竟然没写一篇技术文章,整个上半年工作比较繁忙,下半年节奏缓下来之后也因为太久没动笔了就一直没动笔,可见懈怠是会习惯的。于是趁着年前,把今年工作中做的一些比较有意思的东西梳理一下做个总结吧。这篇文章就先讲讲今年着重做的页面性能优化上的一些经验。

背景

现有业务中有个的3D场景的预览页一直在进行功能迭代,但是一直没有关注页面的白屏时间和首屏时间,平时自己用手机打开也挺快的,偶尔接到反馈说页面加载时间太久的也没怎么重视。直到某一天有同一批客户反馈页面打开时间非常久,经过和客户的沟通后发现问题出现的原因是因为他们所在区域的网络环境不是很好。正是因为这个契机,让我们意识到不能以平时自己所处的正常网络作为衡量标准,同时页面打开速度还会受到手机本身性能的影响。而应该满足大多数情况下页面打开的时间是可以被接受的,因此我们决定对该预览页面的白屏时间和首屏时间进行一次彻底的优化。为什么是白屏时间和首屏时间呢,因为这是最能反馈出页面的用户体验的两个指标,用户对页面好坏的第一感受就在于这个打开时间。

目标设立

我们找到了行业内的标杆产品的页面和我们自己产品的页面进行对比。在公司内的页面性能测试平台上,选取了中端机型,中等网络等相同的测试条件下进行了多轮对比测试。结果让我们尴尬的是,在这一相同条件下,友商页面的平均数据是:白屏时间500ms左右,首屏时间:3.5s左右。而我们的页面数据是:白屏时间2s左右,首屏12s左右。面对较大的差距,在惭愧没有关注页面性能的同时,也为我们找到了优化的目标:在优化后,还是在这一相同测试条件下,我们页面的白屏时间和首屏时间不慢于业内的标杆竞品。

优化白屏时间

关于白屏时间,我们页面原先的逻辑是这样的:当页面中的获取配置的接口(暂且称为getBasicConfig接口,获取页面的标题、logo等基础配置)返回后,拿到logo图片的配置url后加载logo图片。看上去没什么大问题,白屏时间的消耗主要在两个地方:

  1. 页面index.html加载后,index.js和index.css加载耗费的时间(单页面应用,产物为index.html,index.js,index.css)
  2. getBasicConfig接口耗费的时间

因此基于这两点进行针对性的优化,首先看第一点,为了节省文件资源加载耗费的时间,我们的做法是把加载logo的逻辑放到index.html中,这样就不用等到其他两个文件加载完成就可以进行logo的加载了。具体的做法也很简单,在html中直接写logo的css布局和压缩后的js加载逻辑,大致的效果如下:

再看第二点接口耗时的问题,其实我们这里的业务是因为需要支持用户自定义logo的设置,所以需要从接口中拿到logo配置,如果是固定的logo根本就没有这步的优化了。为了解决这个问题,同时考虑到后面的首屏时间的优化,去接口化是一个很好的方式。鉴于我们的页面渲染走的是服务端渲染,且这个页面配置的接口没有敏感信息,因此最终我们去掉了getBasicConfig接口,而是将数据通过服务端渲染时塞一个全局配置 window.config 进来,节省了getBasicConfig接口响应的时间耗费。这样在html中就能取到当前页面的logo配置url了。

通过这两步的优化,页面的白屏时间等于完全取决于index.html的加载时间了,而index.html的加载是最快的,优化完成后我们的白屏时间在同等测试条件下成功缩小到500ms以内。总结一下白屏时间的优化主要是两点:

  1. 将logo的加载提前到html中进行,而不是传统的js文件逻辑中(如果非要在js文件逻辑中,可以考虑如何减少js的体积,也能起到优化效果)
  2. 如果有接口的依赖,尽量去掉接口的依赖(通过服务端数据注入,或者在业务允许的情况下写死页面logo地址)

优化首屏时间

首屏时间的优化是个非常复杂的过程,这里补充一点就是如何定于首屏时间其实也是一个关键的点,是采用fcp或者lcp,还是自己根据业务自定义一个埋点时机作为首屏时间,这里面也有一些讲究。这里就不具体展开了,我们页面的首屏时间选取的是3D页面渲染完成的时间点,通过自定义埋点拿到,具体的表现大致就是logo加载完成的时间(logo消失的时间就是监听到3D页面渲染完成的事件触发),这就很容易从对比h5性能测试平台上,每一帧页面的截图序列中清楚获取到,logo结束的下一张截图就是我们对比的首屏时间。

经过一系列的优化手段,最终我们页面的首屏时间也成功达到了预期目标。下面就基于页面的整个加载过程,具体聊一下首屏时间的优化点:

  1. 压缩index.js的体积,页面一开始就会加载index.js,其加载时间直接算到首屏时间中,因此首当其冲需要做的是优化index.js的体积(index.css一般都比较小,优化意义不大)。我们页面的index.js的原来大小在1.2mb,这明显是有很大优化空间的,经过优化后,页面的包体积成功从1.2mb 降低到750kb。包体积的优化的一些关键点:

    第一步:通过可视化包体积分析工具,从体积占比从大到小找到需要优化的目标
    第二步:进行针对性的优化,从以下一些角度寻找优化点:

    • 大文件模块的同类替代

      • case1:找能能满足需求,但是体积小很多的同类模块包替换
      • case2:如果引入某个包只用到了其他的某个方法,可以考虑去掉该包,自己手写这个功能
    • 大文件的cdn引入替代npm包(cdn引入会带来http加载的耗时,因此效果受到文件大小的影响,原则上越大的文件效果会越好,真实效果需要实际测试)
    • 工具包的按需引入(如antd、lodash)
    • 静态资源图片的处理

      • case1:大图片cdn化(同样面临http加载的耗时,真实效果需要实际测试)
      • case2:小图片的base64化处理(大图片的base64化后在页面中的解析耗时会较长,因此真实效果需要实际测试,小图片肯定有效果)
    • 无用代码的去除,检查代码中是否有冗余或者无用代码
    • 无用npm包的去除,同样需要细心检查
    • 打包配置中生产环境下开启console.log和注释代码的自动删除,去掉sourceMap配置
  2. 大文件的压缩,因为页面中有一些需要从oss上读取的3D模型相关的obj、gltf文件,以及贴图jpg文件,所以这一块的逻辑其实跟index.js一样,都是通过减少资源体积的大小从而减少http响应的时间,来缩短首屏时间
  3. 网络请求优化,网络请求直接与页面资源的响应时间挂钩,因此这一块的优化是一个重点,可以做的优化项比较多。这一块的原则也可以概括成2点:

    • 尽量减少不必要的http请求
    • 必要的http请求下,加快请求的响应

    具体来说,我们页面中做的工作如下:

    • 接口请求优化。

      • 尽量减少页面loading结束之前的接口请求数量,前面白屏时间优化提到的去掉页面基本配置信息的接口getBasicConfig,将数据通过服务端渲染注入到页面window下就是一个例子
      • 接口的异步加载优化,业务迭代的时候大概率接口是一批一批增加的,因此不同批次接口之间可能会出现接口是同步await加载的,可以梳理下这一块逻辑,把没有依赖关系的接口进行并发地异步加载,即用promise.all 同时处理多个无前后依赖的接口请求,节省所有请求完成的总时间
      • 接口的合并,通过梳理所有相关接口,把业务逻辑相关的接口进行合并请求
      • 无用或重复接口的检查,通过梳理所有相关接口,将一些无用或重复的接口废弃
    • js、css请求优化,一方面进行检查是否有无用或重复的资源请求,另一方面默认情况下这两种资源的下载和解析是会造成页面阻塞的,但是大多数情况下这些资源的异步加载是不会影响页面的正常解析的。所以针对这类资源可以进行如下操作:

      • 无用或重复资源请求的检查,删除这些冗余请求。或者将小资源内置到代码中(如一些第三方的css样式文件,如果文件内容不多,可以复制出来放在页面组件的css样式中,减少一次网络请求)
      • script 里的 index.js 加 async
      • script 里第三方 js 资源 加 defer
      • link 里的 index.css 加 preload
    • 在请求链路上预加载资源,合理利用缓存。如果能预判到资源在未来的某个时间点会被访问,那么如果提前利用空闲时间线访问一次,则会因为缓存的效果大大加快后续真正访问时的加载速度。而这就是prefetch 和 dns-prefetch的功效(具体的说明自行查阅相关资料),注意这跟上一点提到的preload有一定区别,前者针对的是当前页面的资源,后者针对的是未来可能访问的页面资源。举我们页面中的实际例子来说明一下:我们的预览页打开的路径其中有一种会经过一个列表页中查看详情进行打开,在这个列表页中包含了查询出来的所有预览页,且这些预览页的域名是相同的。所以在列表页中,我们就可以dns-prefetch预览页的域名地址,prefetch预览页中的重要资源。这样在用户通过列表页打开某一个预览页中,预览页的加载速度会大大提升。
  4. cdn优化,这一块的优化针对的是有大量静态资源需要从cdn上获取的场景,比如我们预览页中就有大量的图片资源需要从cdn读取,因此有着非常显著的效果,具体来说优化手段有下面一些:

    • 如果没接入cdn服务的,接入cdn服务 。很多情况下资源就直接存在oss公共桶上,通过oss的提供的域名地址就能直接访问,如果是这种情形的,接入一下cdn,通过cdn域名进行资源访问,访问速度会有所提升
    • 利用cdn缓存能力,开启cdn缓存和进行cdn预热,加快页面二次访问时打开的速度,开启缓存的方式具体根据接入的cdn供应商而定,一般默认都是开启缓存的。如果没打开的话,可以打开这一配置开关。cdn预热相当于提前访问一遍资源,这样哪怕资源第一次被用户访问也已经能命中cdn缓存了
    • 提升cdn的配置,cdn服务根据具体情况又有更细致的区分,如果成本足够,可以考虑以下措施,对资源访问速度有进一步提升:

      • 提升cdn的节点数量,特别是如果有国内外访问需求,国内的也要考虑东西南北的地域影响。多设立几个节点,且靠近访问量高的地域
      • cdn加速,cdn带宽扩容,加快响应速度
  5. 业务逻辑优化,这一块的需要根据具体的业务场景针对性的采取优化措施,可能没什么优化空间,也可能会起到举足轻重的作用。优化的原则在于:尽可能减少loading结束之前的业务逻辑。举两个我们页面中实际的例子来看一下:

    • 原来在页面loading结束之前,还有获取该场景下“活动轨迹”的逻辑,具体包括获取相关接口数据,处理数据,存在页面store中。而这个数据具体用到的时间是在用户手动点击页面中的一个“开启轨迹”按钮后,所以完全可以滞后这一块的业务逻辑。在页面首屏加载出来之后,再去做这一块逻辑的处理。通过这种逻辑时序的改动,可以加快首屏的渲染速度
    • 原来页面loading结束的时机,是接受到3D场景触发的一个sceneLoaded事件。所以我们研究的重点转为能否提高这个3D场景sdk中事件的触发时机,通过对这个sdk的优化,最终我们成功把这个事件的触发时机提前了不少,从而达到缩短页面首屏时间的目的

优化过程

整个优化过程中抛开具体的优化措施,合理制定优化计划,分阶段进行也是非常重要的一点。一开始一定要找准主要矛盾,影响页面加载性能的因素会有很多,但是每一项因素影响的比重是完全不一样的,这个需要依据测试的数据以及开发的经验去合理判断,并将影响因素按照从大到小的顺序排序,先去解决影响大的因素,解决一个大的影响因素所起的效果很可能比解决10个小的影响因素来的更好。

整个优化过程不是一触而就的,可以划分为多个时间节点进行效果的验证。一方面能及时看到效果,有个正向反馈。另一方面也能有个规划,有节奏的提升优化的效率。

我们页面的这次的整个优化过程大致就分了三个阶段进行:

第一次优化

主要进行了页面js体积包压缩和大文件压缩,优化后,首屏时间减少到 9.5s 左右。

第二次优化

主要进行了网络请求优化,优化后,首屏时间减少到 7.5s 左右。

第三次优化

主要进行了cdn优化和业务逻辑的优化,优化后,首屏时间减少到 3.5s 左右。

感触

在业务快速迭代的过程中,页面性能往往容易被忽视。但是像白屏时间和首屏时间是用户体检好坏的第一感觉,这也恰恰是发挥着前端天然亲近用户侧的优势和职责所在。经过这次优化,我们页面的打开速度明显比之前快了不少,看着前后效果的对比,满足感油然而生,也希望这些优化经验能够帮助到有需要的朋友~


款冬
1.5k 声望42 粉丝

前端小小弄潮儿~