以用户体验为中心的前端性能优化

3,084 阅读25分钟

具体的优化方案内容在第六章

性能优化方案.png

一、前言

技术分享之我见

今年年后两个多月以来,团队投入了不少精力在页面打开性能优化这件事情上,经过一段时间的坚持优化,数据上还是得到了不错的提升。恰逢最近公司在技术中心内部做各类专题的技术分享,自己也受邀代表团队做一次前端性能优化的分享。

对于技术分享的意义,首先作为分享者,必然事先要做大量的准备工作,分析案例、整理各类文档和数据,以保证自己分享内容的高质量。在准备过程中,对自己而言事实上也是一次很好的复盘,能够将自己过去一段时间做的事情从头再完整的梳理一次,大量分散的知识点、技术细节、实现难点,经过体系化的整理,很快就能在自己脑海中组织成一张完整的网,将这些内容有条理的串联和沉淀下来。

而作为被分享者,几场听下来我也是深有感触,很多分享内容中包含着大量的知识点,我们很难能够做到当场完全听懂消化掉,我觉得更多的意义是在于去了解分享者当时在做这件事情时的一个心路历程,以及学习他人解决问题的方式与经验。至于那些细节和知识点,可以先浅尝辄止,如果自己真的有兴趣,也完全可以事后去深究,并且在自己的场景中也去实践尝试。

image.png

我印象最深的一次经历,是在上家公司偶然听到的对无头浏览器技术 Phantomjs 的入门分享,当时只是脑子中大概有一个印象,知道了有这么一个概念的存在。这个简单的概念,在我来到现在的公司后,转化成为公司非常核心的分享海报生成场景的技术解决方案,在业务中被落地,创造了很多价值,后面这个经历也成为了我在公司的第一次技术分享主题。

二、关于性能,我们要解决什么问题

1、存在的问题

白屏久,打开慢,体验差

2、为什么要解决性能问题

几个好的案例

1、亚马逊(美国最大电商公司):通过调查得出,网页打开的速度每快100毫秒,就会让网站增加1%的收益

2、Pinterest(世界最大图片社交分享平台):重建了他们的页面以实现性能优化,使感知等待时间减少了40%,从而将搜索引擎流量和注册量提高了15%

3、COOK(医疗贸易公司):通过将平均页面加载时间减少850毫秒,发现他们能够将转化率提高7%,将跳出率降低7%

一句话总结

一个网站的性能好坏是留住用户和实现变现的基础,也是我们前端技术同学每天都要关注和思考的问题

3、优化项目两个关键阵地

作为一个电商平台,每天产生的GMV是整个公司最为关注的核心指标,GMV = DAU * 转化率 * 客单价,站在前端技术视角,那些能够吸引用户点击访问、下单、最终产生GMV的页面就是我们优化工作的关键阵地。

通过对业务埋点数据的分析,我们很容易就能找到这些关键阵地的所在:

PV流量入口(电商小程序和H5的首页、会场、商详页)

GMV产生入口(CMS搭建的营销活动页,承接超品、爆款、主推商品)

我们针对这些场景下页面的用户体验和性能进行优化,能够最高程度的提升我们的投入产出比,让技术的价值最大化

三、衡量性能的指标

1、业界的指标

在开始进行优化动作之前,我们首先要选择一个或多个指标,用来衡量我们页面性能的好坏。过去,我们用浏览器中 window 的 onload 事件作为我们的参考指标,因为在 onload 事件的触发,代表着我们前端 HTML 代码里书写的资源(包括 HTML 本身、引入的 CSS、JS、图片资源等)已经加载完成,用户端也已经有一个基本的页面骨架呈现,页面的确能够称得上“打开了”,站在技术视角这么去评价也似乎毫无问题。

然而,作为移动终端的电商业务页面,基本的页面骨架以及背景颜色的渲染,事实上对用户而言还不能算是一个可交互的状态,最起码用户在页面上看不到任何的业务信息(无论是会场、活动、或者商品)。因此,如果我们要更加站在业务视角和用户视角去评判我们页面的性能,onload 事件的触发并不能满足我们的需求。

所以我们又重新调研了目前业界常用的一些性能指标,整理下来,大概有以下这些:

性能指标名称含义
DOMContentLoadedHTML 加载完成时间纯 HTML 被完全加载以及解析
Load页面完全加载时间整个页面及所有依赖资源如样式表和图片都已完成加载
FP首次渲染从页面加载开始到第一个像素绘制到屏幕上的时间。其实把 FP 理解成白屏时间也是没问题的
FCP首次内容渲染衡量页面开始加载到页面中第一个元素被渲染之间的时间。元素包含文本、图片、canvas等
LCP最大内容渲染衡量标准视口内可见的最大内容元素的渲染时间。元素包括img、video、div及其他块级元素
FMP首次有效渲染标记主角元素渲染完成的时间点,主角元素可以是视频网站的视频控件,内容网站的页面框架也可以是资源网站的头图等

2、符合自身场景的性能指标

从上面几个业界常见的性能指标中,我们选择最后一个作为最符合我们最真实的用户访问场景的性能指标:FMP(首次有效渲染)时间,此时意味着首屏后端服务接口已经返回数据,首屏前端有效内容完成渲染。

例如下图,三个页面中红框部分完成加载和渲染,我们即可视为首屏有效内容完成渲染。

image.png

秒开率的定义:用户FMP时间在一秒内的占比

3、实现的方式

因为电商业务的页面在交互和设计上具有一定确定性,比如说,在首页的首屏部分,营销活动的CMS资源位会首先映入眼帘,商品详情页则是一张很大的商品主图会占据首屏的绝大部分篇幅,CMS活动页里运营一般都会在头部配置一张一行一的传图组件。因此我们无须去在代码猜测性的去计算出有效的、有意义的模块所在,我们可以根据对业务的理解,直接在我们业务场景的首屏关键元素的渲染生命周期代码中,注入我们的性能埋点,通过简单计算,作为我们整个页面的首次有效渲染数据。

当然,在不同技术栈中,埋点位置的实现会略有差异。

Vue.nextTick(callback) // Vue H5

setData({}, callback) // 小程序

四、优化的理论基础

1、全局调用链路

全局调用链路.png

前端技术领域有一个老生常谈的问题:“从输入url到打开一个页面,都会发生哪些事情”,在我们目前的场景下,按照上图可以划分为三部分:

  1. 首先无论是什么类型的请求,建立一个HTTP连接都是必经之路
  2. 当入口的HTML模版文件下载到本地后,浏览器会渲染基础的HTML的静态数据(很多情况下是一个纯白的页面),同时会根据HTML的内容去下载静态的资源文件,包括JS、CSS、图片等等,这些资源我们目前都是存储在腾讯云COS通过CDN来访问的
  3. 下载好的JS脚本开始执行后,里面会发起大量的异步请求,用来获取服务端的动态数据,中间经过多层服务调用,最终完成页面业务信息的渲染

2、关键渲染路径

无论是静态数据还是动态数据,渲染到页面中变为可见的像素点,都要经历一系列的流程,关键渲染路径指的就是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列

image.png

我们想提高页面打开性能,就需要对关键渲染路径进行优化

3、关键渲染路径的优化策略

提高被加载资源的优先级、控制它们加载的顺序和减小这些资源的体积

1)资源异步化下载,减小请求数量

2)优化必须的请求数量和每个请求的文件体积

3)通过区分关键资源的优先级来优化被加载关键资源的顺序,来缩短关键路径长度

我们后续介绍的各种优化策略,其实都是围绕这三条去做的

五、优化工具

工欲善其事,必先利其器,在做性能优化的过程中,必然要依赖到一些辅助工具,帮助我们更快的发现问题,深入问题和解决问题。

1、性能问题分析

1. webpack-bundle-analyzer

代码模块依赖的分析利器,可以清晰的看到项目打包之后的JS文件体积,并且识别出其中占用较大的部分、或者存在重复引用的模块

image.png

2. Chrome Performance

Chrome 浏览器自带的性能分析工具,可以有效的帮助我们看到页面打开时每个资源加载的顺序、体积、耗时等,并且可以看到过程中每一帧的实时渲染画面

image.png

2、测试实验

优化过程中,我们有时候会有一些实验性质的优化项目,可能在理论上对性能数据会有帮助,但实际上的效果是有待考证的。所以我们一般采用灰度发布的模式来进行线上性能实验,借助微信平台的灰度放量能力,结合我们自己在性能埋点里的应用版本标识,我们很容易测试出优化代码的实际效果。

wecom-temp-f73fbff928df038667aec275d25796c8.png

wecom-temp-99df5050c21e5111f98e8dbd0c958ca5.png

上图就是我们对一次代码逻辑微调优化所做的实验,会先灰度放出很少的量进行验证,如果数据是正向的,我们会继续增大放量,反之则回退代码。

3、性能数据

数据是进行优化项目中十分关键的一环,无论是发现问题,还是解决问题之后的验证,我们都依赖页面渲染生命周期中的性能监控埋点数据。

在这一次优化项目中,我们基于新的性能指标,对团队过去的埋点监控体系进行了完善。包括H5和小程序两套性能监控SDK,基于对performance对象和页面生命周期耗时数据的分析计算,上报我们所需的性能埋点。

应用类型埋点数据
H5DOMContentLoaded、LOAD、FMP
小程序LAUNCH、LOAD、READY、FMP

数据上报完成之后,还需要借助一些可视化工具,通过数据大盘帮助我们实时监控生产环境的性能数据表现,及时的发现问题,验证每次优化的效果。

image.png

六、优化方案

整个优化项目中涉及到的优化点有很多,按优化类型整理分类如下

1、逻辑优化类

思路:提升关键资源请求顺序的优先级

1、data prefetch

首屏接口预请求(首页、会场、商详接口预先请求)

我们一般不用等到进入页面生命周期执行阶段再去调用相应的接口,而需尽可能早的去触发。例如在应用冷启动阶段,页面还没有真正加载之前,就先去预请求数据。小程序中因为每次navigate跳转需要打开新的Webview,我们甚至可以在跳转前提前触发接口,在安卓端跳转新的webview大概需要300ms耗时,完全足够我们预请求到下一页面的关键数据。

2、首屏接口数据动静分离

通过搭建配置出来的营销活动页,可以将活动数据中的静态部分(例如:固定头图的资源地址、楼层结构、楼层组件类型等)抽离出来,作为一个静态JS文件存储在CDN节点,前端可以预先请求和首屏骨架结构渲染,快速进行首屏有效信息展示,待后端数据返回之后,再填充动态数据(例如:商品名称、商品价格、商品标签等)。

如果运营中途对活动楼层进行了修改(低频操作),则会重新上传同名的JS文件,用户本地协商缓存失效,重新拉取新的静态数据。

优化前

image.png

优化后

企业微信20220419-004022@2x.png

关于为什么用JS不用JSON

JSON需要用XHR发起请求,在微信X5浏览器中不会携带协商缓存相关的头,导致缓存无法利用,而使用JS脚本的方式,是通过插入script标签的方式去下载,跟页面中别的JS文件类似,在各个浏览器的缓存利用上表现一致,算是又填了X5的一个坑吧。

3、路由资源提前加载

有时候我们需要在通过接口获取到一些用户身份后再去调用路由跳转,同时触发页面路由JS文件的下载,会存在下载的等待,所以此处也需要根据业务场景预加载一些路由资源文件。

4、设置DNS预解析和TCP预连接

在 HTML HEAD 中添加 dns-prefetch 和 preconnect,可以提前发起DNS解析,TCP三次握手和TLS协商(如果是https的话)这些操作,为后续操作节约一定的时间,这个通过性能试验后我们并没有发现有明显的优化效果。

image.png

5、head preload/prefetch

HTML HEAD 中可以操作的空间还有很多,例如添加 preload 或 prefetch 可以让浏览器帮我们自动在预加载一些资源,webpack 可以通过引入 preload-webpack-plugin 插件,设置这种方式的资源预加载。不过需要注意这两种方式使用场景的区别,preload 用于对当前页面重要资源的提前加载,prefetch 则用于对未来要加载的页面内资源的预加载。

2、接口请求类

思路:减少资源请求次数或体积

1、清理接口的前置依赖

会场、商品等场景需要依赖前置接口返回的内容,判断不同的用户身份、业务场景要调用不同的服务或者返回不同的内容,但这些前置依赖现在可能已经废弃,或者可以在全局统一获取并缓存,无需每次重新调用

2、业务接口拆分

对首页接口拆分,使得真正的首屏部分接口数据体积变小,并且去除了首屏部分前置用户身份依赖判断,使得前端可以在获取用户身份之前就去预请求首屏接口

3、无用字段清理或拆分

接口调用链路优化,例如原接口返回字段中有一个用户的海报图片地址,需要额外调用海报生成服务,拖长了接口RT,而在用户实际展示场景中海报图片并不会在首屏展示,将海报数据异步化,接口从150ms优化到35ms

3、代码打包类

思路:减少首次加载资源的体积,非必要资源异步化

1、修正错误的模块引用和打包逻辑

由于过去一些错误的打包配置,导致几个css文件中存在重复引入字体base64内容,统一修改为CDN资源引用后,css体积减少50KB;项目中一些svg资源被打到同一个js包里面,可以改成按页面需求分开打包,公共js体积减少44KB

2、split chunks拆包策略,充分利用网络缓存

单页应用业务代码基于路由拆分并且懒加载,项目二方包、三方包,根据更新频率进行代码打包分离,例如axios、vue等三方依赖,可以单独打包并设置时间较久的强缓存

3、UI组件、业务组件等资源按需引入和加载

保证项目中引入的代码库都是按需引入的,babel-plugin-component 可以帮助我们做到这一点;另外一些不在首屏中出现的组件,也可以改造为动态import的方式异步加载

4、体验优化类

1、关键页面植入骨架屏/静态构建

首页、会场、商详、cms页面骨架屏(将骨架图片转换成base64格式,静态写入html模版内部),避免白屏时间过久,如果页面结构相对稳定,还可以采用静态编译的方式,将固定的html结构写入模版。

而在小程序中,目前已经提供了开箱即用的静态构建骨架屏能力,让我们可以在开发环境根据真实线上页面的渲染结构,自动生成骨架屏代码。

image.png

2、图片渐进式加载

页面中的一些大图资源,尤其是首页的大图资源,我们一般采取渐进式加载的方式,即图片又模糊逐渐变清晰,这样做既能够尽早的加载呈现图片,又能够避免直接下载带来的屏幕闪动

52034C73-94DC-41BF-ACBF-2D7451FBB047.png

3、图片懒加载

在营销活动页这种包含大量图片资源的场景,如果根据后端返回的字段直接进行下载,一瞬间的并发量将会非常庞大,因此务必要对图片组件进行可视区域的监听,只对当前可见区域的图片进行下载。

5、其他优化类

1、小程序专项优化

除了基础的优化方案,分包策略、分包预加载、同步方法优化、setData相关优化等,微信小程序近两年陆续开放了更多的优化能力,与业界标准逐渐对齐,例如分包异步化(类比动态组件)、开启用时注入(类比 webpack 按需加载)、webview 预加载时机控制等,作为微信开发者也需要经常关注这些新的优化点,后面我计划把小程序技术体系的优化方案也进一步详细地整理出来。

2、已废弃组件、业务代码清理

对业务迭代中产生的废弃代码清理,是我们需要长期、定期去做的一件事情

3、HTTP协议的升级

现在大多数系统、业务都已经默认支持 HTTP2了,只不过在小程序中还需要在代码里通过配置去手动开启HTTP/1.1 升级到 HTTP2,这里我们同样通过实验之后也没有发现有优化效果,虽然在DNS解析,TCP连接等前置流程数据上有优化,但是在连接建立之后等待后端返回数据的耗时反而变大,这个问题因为目前服务端全链路监控的缺失,还在进一步排查中。

image.png

4、HTTP响应包的压缩算法

根据实际场景的需求,可以将 HTTP 报文压缩算法由 gzip 升级为 brotli(实验统计,400K 以上CSS、JS文件,换成 br 压缩算法,约能节约流量体积10%左右)

image.png

七、优化结果

1、核心小程序电商业务优化效果

image.png

2、核心H5电商业务优化效果

image.png

3、营销活动页H5优化效果

image.png

八、感受与总结

1、监控先行,数据驱动的重要性

通过这次技术优化项目,充分感知到了数据驱动的重要性,每当遇到一些优化瓶颈或者细节处存在盲点的时候,没有详细的埋点数据支撑,优化工作很难持续进行下去。所以在优化项目之初,我们就先投入了不少精力在数据指标确定和埋点上报的工作上,过程中往往会出现临时暂停优化代码的上线,而是优先完善性能埋点监控的能力。

未命名文件 (5).png

2、技术知识与实践的紧密结合

本次优化项目涉及到了大量浏览器运行机制、小程序运行机制、HTTP协议、网络缓存、CDN 等相关内容,基于这些理论知识,借助各种工具进行了大量的性能实验。

关注细节也是一大要点,性能优化充斥着大量的细节点,小、多、杂,每次觉得这次优化做完,就再没有可做的了,但每次都又能抠出新的优化点,这就需要我们不断关注数据,结合理论知识不断尝试实验。

3、性能VS体验

当我们性能数据上有了提升之后,我们仍然觉得打开的体感不够完美,尤其是首次冷启动时仍然存在较长的白屏,这时候就需要针对体验再进行优化。有时候优化100ms的时间,肉眼很难感觉得到,性能和体验应该是互相补位、互相配合的关系,两者相辅相成,缺一不可。简单来说就是性能不够,体验来凑。

针对体验我们目前主要通过添加静态骨架和业务骨架的方式,尽量减少白屏等待时间,这个时候FP、FCP这些就成为了关键指标。另外页面视觉稳定性指标CLS,是用于量化用户体验到意外布局移位的频率,较低的CLS有助于确保页面用户视觉和交互体验,也就是避免打开时出现大量元素的闪动。

九、后续规划

本次优化面向的基本还是前端侧的可交互时间,而在真实场景中,尤其是社交电商这种重分享、跨终端的业务中,我们还需要关注全链路的用户交互性能体验

1、全链路监控和优化

跨端跳转场景

例如在App唤起小程序、App打开H5、小程序打开H5等场景下的完整耗时

未命名文件 (6).png

冷启动场景

小程序冷启动完整耗时,包括代码包下载耗时、代码包下载率、JS注入耗时等数据

未命名文件 (8).png

2、Data Prefetch

目前针对 webview 下我们已经做了一些优化,例如在后台预加载一个容器或直接加载 Tab 页的 H5 页面,但这些方案并不能解决我们在所有场景的问题,还存在一些待优化项。比如我们可以在客户端路由跳转前去预请求服务端数据,前端加载完成后通过 JSBridge 进行获取,这样就能比前端自行预请求更早的获取到数据,渲染有效内容。

未命名文件 (10).png

3、客户端离线包

这次优化后,我们发现用户在首次打开时,仍然存在较长的白屏时间、体验差的问题,针对这一场景,客户端内同样可以提前去下载关键的HTML、CSS、JS资源,等到页面加载时则可以获取到本地缓存,这样能够很大程度的避免首次打开的长时间白屏等待。

十、业务思考

1、以用户体验为中心的性能优化

指标确定

对于此次性能优化核心指标的确定,我们摒弃了传统的浏览器DCL、Load事件作为衡量标准,因为纯前端视角的HTML结构解析完成、资源完成加载对于用户而言还远远没有达到一个“有效信息渲染”、“可交互”的状态,所以我们引入了FP、FCP、FMP这些指标,让我们的量化数据与真实的用户体验更加接近。

当然,用户体验除了简单追求打开速度上的快之外,顺滑的呈现体验、以及后续交互的响应速度也同样重要。前文提到的CLS就是对页面视觉稳定性衡量的一个重要指标,响应速度我们可以通过追踪输入延迟,用户输入是否能在100毫秒内响应来判断。

数据分析

对收集上来的性能数据进行分析,我们这次使用了秒开率作为我们的优化效果评判依据,因为在一秒内就能完成FMP的话,我们认为是一种性能较好的体现。

我们没有采用FMP的平均值作为标准,是因为在不同的机型、系统、业务场景下,即使是同一个页面也会有不同的性能表现,简单的求平均值会将一些潜在的问题隐藏。因此我们后续又补充了下面的百分数位统计图和分布直方图,更加精确地分析前端页面的真实性能表现。

image.png

简单分析就能够发现,末尾10%的用户FMP时间,和前90%的用户之间存在着很大的差距(2000ms ~ 3000ms),也就是说还存在10%的用户感受着“糟糕的体验”。

image.png

而当我们进一步去分析这后10%的用户时,发现绝大多数是微信端用户(包括公众号H5和小程序)和安卓端用户,具体原因除了操作系统本身的性能差异之外,微信客户端还会对用户请求做风控拦截的缘故,导致请求的整体响应速度变慢。那么利用客户端缓存,避免过长时间的请求等待便是我们接下来在小程序端的重点优化方向。

2、标准通用能力的沉淀

这次优化项目中效果最显著的两个单项是data prefetch和静态数据的CDN化,分别给我们的应用秒开率带来了10%以上的增长,对于这两项能力,我们还需要将其沉淀为前端的通用能力,使其可以在别的项目中进行快速复用。实现的方式则是在项目的脚手架和基础开发模版中添加相应的配置开关,使开发者可以自由选择是否使用这项能力,并且在业务中可以做到灵活配置。

3、技术优化与业务价值的关系

首屏加载是用户体验的开始,好的开始是成功的一半。但是在我们的场景下目前还没有做到,可能也很难做到直接用GMV的数字去直接量化性能优化带来的业务增长,但是我们可以进一步引入一些中间指标,将性能指标和业务指标关联起来。

秒开率(次数): 在这个页面一秒内完成FMP的次数/页面打开次数

跳出率(次数): 在这个页面离开小程序的次数/页面打开次数

次均访问深度: 日访问页面数/日打开次数

次均停留时长(秒): 在这个页面用户产生的总停留时长/页面打开次数

我们可以通过优化FMP提高秒开率以及降低因等待过久导致的用户跳失,通过内存优化和交互体验上的优化,让用户能够访问更多的页面层级、在页面上停留更长的时间,用这些指标的数据去间接地体现技术优化的价值,而这一价值会贯穿到拉新、留存、促活、转化、分享等各个业务增长环节中,最终对业务GMV产生贡献。