前言
数据表明, 即使在资源有缓存的情况下, 页面首次访问的耗时也是非首次访问的两倍
为什么首次访问这么耗时呢, 时间去哪里了? 本文详细分析页面首次访问耗时的原因
常见的初始化
我们先看看打开一个页面, 需要经过那些流程可能会包括, 外壳初始化, 内核初始化, 创建 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, 在业务初始化方面不一定会有性能问题, 需要根据自己的实际场景, 具体问题具体分析
来源: https://juejin.im/post/5ab85daa5188255587238a04