前言
Web页面的体验,特别是性能体验,一直饱受诟病。在和Native比较时,我们往往避其锋芒(性能),宣扬Web的跨平台,快速迭代,容易推广,开发成本低等等特性。
但是,Web的体验真的很差吗?一些页面实践表明,深度优化的Web体验完全可以媲美Native。比如,支付宝ofo小黄车完全基于Web技术去实现,经过深度的优化,在最新的实验室版本性能已经超越微信的版本。
本文介绍手淘,支付宝,钉钉,等等集团内重量级App中Web性能优化的实践,抽出恶化Web性能的元凶,展示如何打造极致的Web体验。
期望大家看了文章之后,自己实操一下,能提升页面30%的性能。
Web优化的基础
数据是Web优化的基础,关于数据的话题,我们能讨论的内容非常多。
下面以Chrome V8引擎为例子,介绍一下数据是如何影响性能优化方向的。
上图展示的是Chrome V8团队官方的数据,
JS性能测试 |
JS Execute & GC |
Parse & Compile |
基准测试 (Octane) |
> 70% |
< 10% |
真实页面 |
< 35% |
> 30% |
我们可以非常明显的看到,在 Octane benchmark 基准测试中,JS Execute & GC 占了70%以上,而JS Parse & Compile 只占了不到10%。所以,Chrome V8团队在2016年之前一直采用双编译的架构(Full-codegen+Crankshaft/TurboFan),重点优化编译器,编译出最高效的机器码。这种策略,让V8引擎一直在实验室上(Octane benchmark)领先世界。
然而,真实世界是残酷的,真实页面的性能,JS Execute & GC仅占不到35%,而JS Parse & Compile的时间则超过30%,在一些含有大型JS的页面,解析编译的耗时甚至可以超过70%。
在真实页面上,V8的性能远远落后于JSC,Chrome V8团队也逐渐意识到了问题。在2016年开始使用真实页面去测试V8的性能,在架构上把解析器( Ignition)加回去,并在59版本默认开启,重点优化解析和编译过程的性能。
从上面的例子我们可以看到,数据是非常重要的,可以影响一个大型团队几年的技术方向。如果想在优化方向上不走偏,拿到一手好数据是非常关键的。
Web优化的工具
工欲善其事,必先利其器。在性能优化方面,一般有那些比较好的工具呢?
一般有三种类型的工具,
(1)基于网络抓包的工具
比如,各种抓包工具,WPT,UPT,等等。
(2)调试页面的工具
比如,Chrome Devtools里面的各种Debug工具。
(3)基于Trace的工具
Trace的原理是在浏览器内核的关键函数进行打点,可以分析内核函数执行的次数和执行耗时。理论上,只要打点准确和完善,它能分析一切的页面性能问题。
这类工具有Trace,Timeline,JS Profiler,Lighthouse,WDPS Lighthouse,等等。
至于这些工具如何使用,请自行Google。
Web优化实践 - 资源加载
上面说了很多虚的,我们来一些真实的案例。
在资源加载方面,有两个方向,一类是走网络,另外一类是走缓存。
如果资源走了网络,它的时间消耗在哪里呢?除了我们能很直观的理解到的,资源大小,大的资源肯定耗时更多,还有什么呢?还有,域名解析(DNS)耗时超过200ms,创建https连接耗时超过600ms。首屏如果需要全新建立网络连接请求资源,一般就非常难达成秒开的目标。
走网络这么耗时,不如都走缓存?的确如此,目前很多App上页面都直接走缓存。走缓存的方式也非常多,比如,我们可以通过设置Cache-Control让浏览器标准的HttpCache缓存下来,也可以使用JS把文件写到LocalStorage,也可以使用ServiceWorker的Cache API,当然,更厉害的是可以预加载,甚至直接下发离线包通过shouldInterceptRequest返回给内核。
从上面可以看到,资源加载的优化方式非常多,很多技术都可以提前将资源下载到本地,资源加载已不是性能优化的瓶颈。不是瓶颈,不是说它不重要,而是指我们有很多手段去解决问题。
Web优化实践 - 排版
在解析排版方面,我们先看一个例子,
上图中,页面JS不断的往主文档插入CSS,浏览器内核发现CSS变化了,就会解析CSS,进行样式重计算,甚至会引起重排版。这是一种非常低效的处理方法,比较好的实践是,让CSS成为一个独立的文件。
从上面的例子可以看到,错误的使用Web技术,可能会引入严重的性能问题。
Web优化实践 - 业务
在看页面性能时,我们往往更加关注一些比较牛X的技术,很少关注具体的业务逻辑,更加不会想推动业务修改逻辑。但是,很多严重的性能问题,往往是业务的实现方式引起的。
我们先看看例子,
上图中,框框里面空白的地方,浏览器内核在干嘛了呢?事实是啥也没干,它在闲着呢。
这个时候,其实是业务在干事情,地理位置定位 → 获取授权 → 换取授权,这个过程耗时约2秒,在此之前,页面不会展示任何内容。但是,这个过程事实上并不是必须的。
很多人会说,业务实现有性能问题的毕竟还是少数。我再举一个例子,集团的埋点校本(aplus_wap.js)里面的sendPV函数耗时可以超过100ms,原因是它使用new Image的方式去上传统计,为了保证上传成功率,必然需要一定的延时,更优的方式是使用Navigator.sendBeacon,它可以保证在文档关闭的情况下数据也能正确上传。
很多时候,业务逻辑稍微一调整,页面性能优化/恶化 30% 是很有可能的。
Web优化实践 - 渲染
很多时候,我们在PC Chrome或者iPhone X上,页面首屏出现的非常快,而在Android上却出现的非常慢,除了机器性能上差异之外,是否还有其它原因呢?
的确是有的,很有可能是业务的非首屏逻辑提前插入执行了,导致首屏渲染被延迟。我们先看一份Trace数据。
上面Trace中,红色框框的是非首屏逻辑,蓝色框框的是首屏渲染。
非首屏逻辑插入为什么会阻塞首屏渲染呢?JS是单线程的,而且是在内核线程执行,它的执行会占用内核线程,必须等它执行完才能继续渲染页面。
那么,这个问题为什么不会在PC Chrome或iPhone X上出现呢?其实在所有机器上都有可能会出现,只不过前端习惯于使用PC Chrome进行调试,他们将非首屏逻辑进行一定的延后处理,在PC Chrome上调试通过了,就认为是正常了,但在相对性能较差的Android机器上,非首屏逻辑还是插在首屏渲染的前面了。
这类问题更深层次的原因是,前端其实得不到准确的首屏时间点,只能以DOMReady,LoadEventEnd,等事件近似处理,即前端也不知道什么时候才应该执行非首屏逻辑。
很不幸,这类问题目前还未有很好的解决方案。一种方案是,内核在Devtools上将首屏(T2)的时间点标记出来,让前端能及时发现问题。
Web优化实践 - 开发流程
我们日常使用的开发流程,比如,打包,混淆,这些是否会带来性能问题呢?我们看看例子,
上图中,JS执行耗时2秒多,这应该是一个非常巨型的JS吧?事实上,这个JS不到50K,逻辑也非常简单,优化之后仅仅15ms就执行完了。
那么,它的性能为什么会这么差呢?事实上,它是由JS混淆引起的,这个JS使用了非常高强度的混淆,将一些函数名称都拆解成了字符串拼接。
JS混淆是非常常用的,但是,如果我们刻意去追求隐私性而忽略性能,往往也会引入严重问题。而这些开发流程层面的问题,往往更加严重,因为会影响到非常多的页面。
Web优化实践 - JS解析
很多页面,我们都会看到一些非常大的JS,比如,超过300K,甚至超过2M。甚至一些通过各类工具让JS变大,以能驾驭巨型JS为荣。这些巨型的JS会有什么问题呢?
我们先看看例子,
上图的JS执行在U4 2.0耗时约3秒,在U4 1.0则要5秒,即使是在PC(i7 CPU+32G内存)上也要800ms。
为什么这么耗时?我们可以看到框框里面很多密密麻麻的青色线条,内核在干什么呢?那是内核V8 JS引擎在解析编译。
现在前端渲染非常流行,大部分页面逻辑都由前端JS去实现。很多页面都会有几个很大的JS,有些是业务逻辑复杂,有些是引入了复杂的前端框架。HTTP Archive 统计数据表明,页面平均JS大小由2010年的110K上涨到2017年的450K。
一般来说,Nexus5上每100K的JS源码需要消耗100-200ms的解析编译时间,降低页面JS大小是一个非常好的性能优化实践。需要注意的是,不同的机器解析编译时间差异极大,iPhone8可能比Nexus5快好几倍,一般建议使用中低端机器进行性能测试。
那么,怎么确定页面的JS是否可以减小呢?Chrome 59 Devtools 新增了Coverage Tab,可用于判断页面JS代码实际被使用的比例。
Web优化实践 - V8 Cache
那么,如果页面JS大小降不下来,还能不能优化呢?U4 2.0 在这方面做了较大的优化,能持久化JS引擎的解析编译结果,在后续访问都能重用,我们看看在U4 2.0上用上了V8 Cache的效果,
我们可以非常明显的看到,同一JS,使用V8 Cache之后,页面JS执行性能由3秒下降到800ms,降了差不多4倍!
有没有一些JS是用不了V8 Cache的呢?我们在内核层面几乎没有限制,但JS的一些写法可以让V8 Cache失效。
从上图可以看到,客户端进行JS注入时,往JS内容插入了一个时间戳。即使每次注入的JS是一样的,但由于插入了这个时间戳,JS二进制内容发生了变化,JS引擎在进行V8 Cache的时候会认为是一个全新的JS,会重新生成V8 Cache,无法重用之前的V8 Cache,即等同于V8 Cache完全失效。
前端在缓存问题上是非常纠结的,有时候恨不得全部资源都能缓存,有时候又期望浏览器千万不要缓存,比如,设置no-cache,URL添加哈希值,甚至往资源插入时间戳。
但是,无论如何,千万不要动态往资源内容插入时间戳,这是完全没有必要的,它会秒杀一切缓存。
看了JS相关的实践,如果大家自己页面的首屏主路径上有大型JS,请自觉回去将JS的大小降下来或升级U4 2.0,页面性能会获得出乎意料的巨大提升哦。
Web优化实践 - 容器
很多页面都运行在集团的超级App里面,这些App的容器对页面性能是否会有影响呢?
我们先看看一些实际的例子,
上图可以看到,容器在处理shouldInterceptRequest的时候,会经过一个消息模块抛转,再处理一大堆业务逻辑,最终导致结果在50ms之后才返回。如果页面有20个请求,那么耗时就1秒了,离线的优势就完全丧失了。
再举一个例子,前端通过JSAPI调用容器的接口去获取一些客户端的信息,这是非常常见的。但是,一些容器JSAPI的性能非常差,一个接口调用耗时竟然能超过200ms,一个页面十来个调用,性能就很差了。
容器对接的是内核WebView,如果在WebView的一些回调接口处理不高效,可能引入严重的性能问题。集团一个App的容器进行了一轮WebView相关回调接口的性能优化之后,全部Web页面性能提升了10-15%。
Web优化实践 - 初始化
很多时候,打开页面很慢,是因为有很多事情我们没有提前去做。
我们先来看看一些例子,
从上图可以看到,new WebView是有成本的,约10-1000ms,为什么差距这么大呢?全新安装首次创建WebView耗时约1000ms,重启浏览器首次创建WebView耗时约300ms,第二次创建WebView则10ms就完成了。
在load so,jar,等方面耗时也比较大,还有浏览器内核的各种初始化流程,比如V8 引擎的初始化,ServiceWorker线程的启动,这些都是有成本的。
我们在了解到了这些成本之后,可以怎么做呢?比如,我们可以起一个隐藏的WebView,让它先加载一个本地页面,走一遍内核的流程,这样,在用户真实访问页面的时候,就会非常流畅。
总之,我们可以有各种思路(预加载,预连接,预创建WebView,预创建进程,预执行JS,预渲染,预读,等等),去提前做一些事情,避免用户需要全新创建WebView去访问页面,这样就可以大幅提升体验。
结束语
前面我们介绍了非常多的性能优化实践,这些实践都是在集团巨型业务中积累总结的,希望可以给大家开阔Web体验优化的思路。大家在看完这些内容之后,也可以默默的检查一下自己的页面或App,有没有类似的问题,比如,如果页面首屏主路径有大型JS的,将那些JS大小降一半,可能你们年度的性能优化目标就实现了。