浅谈面向客户端的性能优化

有朋友通过《 智能音箱场景下的性能优化 》一文找到了我,既然智能音箱的性能优化相当于一个超集,那么对其的一个子集——客户端系统如何进行性能优化呢?

反正隔离在家,不妨对客户端的性能优化梳理一下。

我思故我在

首先,回顾一下性能优化。性能优化是面向时间的艺术,简单的说,就是在不影响系统正常运行的前提下,执行得更快,完成特定功能所需的时间也更短。关于性能为王的更多描述,可以参考《深入分布式缓存》一书。

客户端系统的性能优化可能是一种不太准确的说法,所有的性能优化都是为了更好的用户体验,客户端系统的性能优化大概是指如何优化客户端系统已获得更好的用户体验。然而,客户端不是独立存在的,面向客户端系统的性能优化同样需要服务端的配合才成。

既然是提升用户体验,就需要抓住重点,哪些才是性能问题的关键部分呢?客户端系统在应用整体性能中处于怎样的地位呢?客户端优化对整体性能的影响大么?

实际上,很多时候,性能的瓶颈确实是在客户端,老码农对经历的多个系统进行过时延分布的统计分析,客户端对整体性能的影响接近80%,又一个二八原则出现在了面前。

也就是说, 客户端的性能优化大概率对系统能有着决定性的影响。

客户端的性能分析

客户端又有着宽泛的概念,和大前端类似,包括了App,Web前端,小程序以及hybrid App等等。客户端的性能优化同样需要找到关键指标,用数据说话,并实现可持续的优化,监控、记录、分析、优化的这一路径依然有效,而且必然有效。

用户使用客户端,在与客户端交互之后,等待客户端从后台服务器获取内容并呈现:

  1. 客户端的本地预处理

  2. 网络处理:包括查询DNS,建立连接,发送请求到服务器,并等待响应

  3. 可能是用户在客户端上的白屏时间,当然可以用本地缓存来改善体验

  4. 客户端开始接受数据,解析内容

  5. 对一般的前端而言,客户端将读取缓存,并执行JavaScript/CSS等,这对于用户的首屏时间

  6. 客户端处理数据,计算布局,并开始渲染屏幕等,这时用户就处于可操作的状态了。

以Web前端为例的话,白屏时间一般到结束,首屏时间到首屏图片加载结束,用户可操作时间要到DOMReady/核心JS加载完毕。

因此,客户端的性能优化可以分为两大部分:面向网络通信的优化和面向客户端自身的优化。

面向客户端的网络优化

根据一般的经验, 网络通信是系统的性能瓶颈,IO慢,而最慢的IO就是网络通信。网络性能优化主要包括:减少网络请求的次数,减小网络请求的流量大小和提升网络请求速度。

减少网络请求次数

削减请求次数是提升通信性能的必经之路,就Web前端而言,过往的一些经验是

  • 少使用iframe,避免404和空的src

  • 缓存Ajax的结果,尽量使用Ajax GET

  • 域名拆分,减少DNS查询次数

  • 避免跳转,减少HTTP连接

  • 采用延迟加载和预加载

减少请求次数往往涉及到业务流程的改造,或者涉及协议的合并与重组,有时候,还会和减小网络请求内容大小的方法相冲突。

减小网络请求的流量大小

网络请求的负载一般是很难减小的,相反,随着业务的增长,协议的膨胀,网络请求的内容大小还会增长。对于网络请求的净负载而言,序列化可以在一定程度上压缩数据内容的大小,例如使用protobuf。

另一方面,由于gzip几乎已经成为了基础配置,所以网络请求中的具体数据会采用gzip压缩,并在压缩后传输,然后在客户端上解压处理。特别地,对图片而言,可以使用webp等格式对图片进行压缩,或者使⽤CSS Sprites也是一种办法。

既然网络中的数据内容难以减少,就要尝试减少内容的无效传输。这是缓存技术的又一典型应用, 服务端配置Expire/cache-control,配置ETags等等都基本上是规定动作了。

提升网络请求速度

网络带宽一直是短缺资源,但并不是我们无法提高网络请求速度的借口。例如服务端可以尽早flush数据,使用CDN更是不二法门,尤其是全站CDN,有时候能解决大部分的性能问题。

根据长短连接的特点,在客户端选择长短连接同样可以在一定程度上提高请求的速度。一般地,长连接适用于点对点通信,节省TCP建立的耗时,在首次建立连接后,多次交互复用;短链接适用于高并发通信,节省socket资源,每次交互重新建立链接。

对于Web前端而言,了解浏览器所支持的连接数,可以帮助我们在带宽充足的情况下同时使用多个连接进行通信。

浏览器 HTTP1.0 HTTP1.1
Chrome 6 6
Firefox 6 6
IE8 6 6
Opera 4 4
Safari 4 4

表中的数据大约是4年前的了,仅起到参考作用,技术演进很快,目前的浏览器对连接数有着自己规定和限制。

同时,连接池有时是性能优化的必然选择。如果每个请求都会发起一次连接的话,会:

  • 增加延迟:域名解析、TCP握手、SSL握手、TCP慢启动

  • 消耗流量:域名解析、握手挥手等

建立连接池可以保存空闲连接,请求优先去连接池找空闲连接,连接池命中失败再发起新连接,可以实现Socket Late Binding,具体可以参考《从连接池到内存池》。

面向客户端的自身优化

客户端自身的优化,除了业务逻辑的优化之外,主要是缩短显示布局和渲染的时间,让用户感觉上“更快”。

web 优化

就web前端而言,如果使用了cookie, 需要考虑cookie大小,静态域名最好不带Cookie。提升CSS的性能,一般在代码中置顶CSS,使用代替 @import ,避免CSS表达式和Filters。在SPA类型的应⽤中,要减少CSS的3D加速,减少CSS往往比减少Javascript更重要,因为渲染的时候内存往往比CPU重要。

Javascript 的优化方式一般是前端工程师所熟知的,例如,脚本置底,JS和CSS外联,压缩并去掉重复脚本,减少DOM操作,使⽤事件代理等等。进一步,是数据结构和算法的优化,例如,减少查找,避免with、eval和动态改变对象属性,使用数组来进⾏字符串拼接等等。

另一个有效的方式是减少DOM的数量,但实际上这对设计的要求很高。但是,Anyway,要减少DOM的重绘触发,避免访问 childNode的 数组,采用读写分离,收回并重复利用DOM,缓存数据⽽不是DOM本身。

App 优化

一般地,App 比web前端更能充分地利用端能力。Web页面的生命周期较短,一般是单线程,而且持久化能力较弱。App UI 的生命周期更长,支持多线程,有工作的优先级,具备较强的持久化能力。

App 的优化一般包括预加载内容以及首图, 最小化首屏网络请求。根据用户操作,提前预判处理。同样,可以预先下载资源包,将资源静态化上CDN。App可以通过端做连接加速(如长连接),SDK提前装载以缩短初始化时间,并对图片等资源进行缓存。

App同样可以包含Hybrid 的形式, 对类web的呈现可以通过模板本地化来预渲染,可以预初始化webview,预取端能力的执行结果。为了减少Javascript与Native 的交互,可以使用React Native来完成渲染,可以 使 用户体验得到一定的优化。

当然,App还可以对内核做针对性的优化。

客户端的性能评估

性能优化和策略工程是类似的,没有效果评估的性能优化都是耍流氓。

但是,客户端的性能评估存在着一些挑战,例如除了客户端程序之外,还有其它程序在运⾏,JS引擎也有着JIT等动态优化,还有着各种不可⻅的缓存(如I/O、CPU cache等)。性能的评估多是基于日志埋点,一个良好的日志埋点系统最好能过支持按需定制的后埋点配置方式。对于客户端系统而言,还可以有其他多种测量和评估的手段。

性能问题的定位主要是模拟出现问题的场景,如高并发,高负载情况下的系统反应。对服务端而言,一个性能测试平台,尤其是压测系统,一般提供两个功能,压力产生和数据收集。数据收集模块负责收集数据,支持用户自定义的数据收集逻辑。压力产生的模块负责多进程的管理,执行用户的发压逻辑(如rpc,数据库操作),支持自定义发压逻辑。例如,可以在里面实现一个写数据库的操作,也可以实现一个读文件的操作等等。

通过压测,可以发现稳定性的问题,包括并发操作/访问带来的接口内部逻辑的异常(如多线程逻辑异常,死锁,内存泄漏等),更主要的是发现性能问题,包括吞吐量,qps是否达到上线流量的要求等等。

对于客户端而言,一般采用类似Monkey那样的压测工具,或者编写定制的测试工具来进一步完成对性能的评估。对于类似手机上的应用软件,一般会压测72小时,当然,最少也要压测4~8小时。

另外,客户端系统的性能优化除了运行速度之外,还有对端上电量、CPU/内存等资源的考虑,从系统层面来看,都属于性能优化的范畴。

参考资料与关联阅读