前言
数据表明,即使在资源有缓存的情况下,页面首次访问的耗时也是非首次访问的两倍。
为什么首次访问这么耗时呢,时间去哪里了?本文详细分析页面首次访问耗时的原因。
常见的初始化
我们先看看打开一个页面,需要经过那些流程。可能会包括,外壳初始化,内核初始化,创建WebView,创建Renderrer进程,初始化V8 JS引擎,初始化IPC,初始化CC,初始化网络库,初始化文件系统,初始化数据库,启动ServiceWorker线程,DNS解析,创建网络连接,页面服务器初始化,等等。这些流程前端一般是看不见的。
在讨论具体的耗时之前,我们先约定,下文所有的数据都是基于Nexus 5手机。不同的手机的性能数据差异极大,一些高端手机(比如,iPhone X),性能可能是中低端机的好几倍。
外壳初始化
我们先看看浏览器外壳的初始化,用户点击桌面图标启动浏览器,浏览器会进入一个状态机,按步骤初始化各个模块,很多模块的初始化会涉及网络,文件IO,JNI,等等操作,这些都会有一定的耗时。
当然,全新安装首次启动,外壳初始化的过程中,一般最耗时的是加载SO和JAR,其中使用DexClassLoader去加载JAR文件,在一些中低端机器,特别是Android 5.0以前的系统,耗时是以秒计算的,有些甚至可以达到10秒。非全新安装首次启动,加载SO和JAR的耗时会大幅下降,大概在500ms。
我们为什么需要关心浏览器启动的耗时呢?一些场景下,用户通过扫码或者点击桌面图标去访问页面,这个过程就会包含浏览器的启动流程,我们有必要了解这其中发生了什么。
对于内置浏览器内核的App,比如,支付宝,手淘,情况又是怎样的呢?我们这边暂时没有支付宝和手淘的启动性能数据,但模块初始化,加载SO和JAR,这些流程都会有,时间不会很小。
在外壳初始化耗时方面,有没有一些比较好的解决办法呢?
最好的办法就是进程保活,现在国内很多手机厂商都会给微信,支付宝,等超级App去进程保活,用户在任务列表杀掉了应用,其实进程还在。
如果是多进程的情况,可以提前创建进程,比如,微信和支付宝的小程序,用户访问时可以直接使用预创建的进程。
内核初始化
我们再来看看内核的初始化,与外壳的初始化类似,内核的初始化也需要加载SO和JAR,创建WebView和初始化各个功能模块。
在创建WebView方面,全新安装首次创建约1秒,非全新安装首次创建约300ms,第二次创建约15ms。
首次创建Renderrer进程,初始化IPC,初始化CC,这些耗时在百毫秒的级别;
V8 引擎相关的初始化耗时也在百毫秒的级别,其中首次NewContext要20ms。
总的来说,首次访问加载SO和JAR一般需要500ms,创建WebView和走完内核流程一般需要消耗500ms,也就是说,提前初始化内核和预创建WebView加载一个URL,跑一趟内核流程,可以带来约1秒的收益。
业务初始化
在页面加载的过程中,内核会有很多回调通知外壳,这些回调的处理上是否可能存在性能问题呢?
我们发现,在一些App上,一些接口很可能会出现性能问题,比如,onPageStarted,shouldOverrideUrlLoading,shouldInterceptRequest。
这些接口为什么会出现性能问题呢?一般很多应用会在首次onPageStarted回调时执行复杂的业务逻辑,比如,初始化一些统计模块,进行JS注入,等等。需要说明的是,onPageStarted并不是同步接口,为什么也会有影响呢?因为它是在UI线程执行的,长期占用UI线程,会对内核有较大的影响,内核很多操作需要抛转到UI线程去处理,比如,ServiceWorker线程启动就有抛转UI的过程,在UI执行完之前,它只能等待。
shouldOverrideUrlLoading 是客户端拦截请求的关键接口,内核会同步等待,很多应用会有比较复杂的拦截规则。
shouldInterceptRequest 是客户端离线包的关键接口,内核会同步等待,很多应用会在这个接口首次回调时去解压离线包和初始化离线模块。
在一些实际应用中,优化这些回调的处理,可以给全部H5页面带来 10% 以上的性能提升。
ServiceWorker初始化
ServiceWorker是PWA的关键技术,它具有非常强大的能力,Fetch,Cache,Push和Add to home screen,能让前端开发者非常灵活的操控页面缓存。
同时,它也是有比较大的初始化成本的,比如,ServiceWorker线程启动平均要200ms,而每次访问页面,一般ServiceWorker线程至少都需要启动一次。当然,Chrome也在不断优化这块的耗时,最终预计能优化到100ms以内。
网络初始化
在网络初始化方面,一般内核网络库的初始化并不太耗时,耗时的是DNS和Connection。
用户首次访问,一般都需要去进行DNS解析和创建连接,而在后续访问时,一般都可以用上缓存或者预连接。
DNS解析,一般耗时在200ms以上,创建HTTP连接,一般耗时也在200ms以上,而创建HTTPS连接则需要600ms以上。
也就是说,用户首次访问时,如果不能提前创建连接,从性能的角度来说,是非常危险的。
这个方面我们的建议是,使用HTTPDNS提前解析和缓存DNS,提前创建连接(比如,用户点击时)。
浏览器也有这方面的优化,比如,在加载主文档时,提前发起子资源的预连接,但在一些托管网络库的应用来说,这些策略可能不会生效。
服务器初始化
页面服务器和资源服务器,是否也需要初始化呢?一般也是需要的,比如,页面访问过之后,页面服务器也会有一些缓存,用户再次访问时可以直接使用缓存而无需走完整的流程,但这些缓存应该是大部分用户都能共享的,所以实际影响不好评估。资源服务器也一样,比如,图床,很多是按用户手机屏幕和网络类型来返回不同图片的,用户访问过就会放到CDN缓存中。
暂时未有数据表明服务器初始化对页面整体性能产生明显影响。但我们有另外一份数据,在一个业务中,预创建WebView提前加载一次模版页面,能让全网平均性能优化100ms。其中,模版页是304的,里面的资源都是可缓存的,也就是说,这100ms的收益并不来于缓存,而是来于某些模块的初始化。
JS初始化
这里提到的JS初始化,并不是前面说的JS引擎相关的初始化。JS初始化是指JS文件缓存到httpcache和解析编译生成V8 Cache文件。很多数据表明,JS解析编译占JS耗时的35%以上,一些有巨型JS的页面甚至可以达到80%。在U4 2.0中,一般JS执行一次之后,就可以生成V8 Cache,虽然V8 Cache可以重复使用,但也存在被自动清理的情况,所以提前执行一次还是有收益的。
一些业务中,提前执行一次JS,在用户真实访问时,耗时从500ms降到200ms。特别是在一些超级App中,基础JS基本都一样,提前执行一次可能会带来非常明显的收益。
结束语
上面介绍了一些常见的初始化对页面性能的影响,希望大家能了解到一些隐藏的信息,能开阔Web优化的思路。当然,这些点不一定会存在很大的性能问题,比如,一些业务模块处理的非常好的App,在业务初始化方面不一定会有性能问题,需要根据自己的实际场景,具体问题具体分析。