前端优化系列 - 基于UC内核的极致Web体验

简介: Web页面的体验,特别是性能体验,一直饱受诟病。在和Native比较时,我们往往避其锋芒(性能),宣扬Web的跨平台,快速迭代,容易推广,开发成本低等等特性。但是,Web的体验真的很差吗?一些页面实践表明,深度优化的Web体验完全可以媲美Native。

前言

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大小降一半,可能你们年度的性能优化目标就实现了。

目录
相关文章
|
5天前
|
JavaScript 前端开发 UED
【Web 前端】如何将一个 HTML 元素添加到 DOM 树中的?
【5月更文挑战第2天】【Web 前端】如何将一个 HTML 元素添加到 DOM 树中的?
|
5天前
|
JavaScript 前端开发 索引
【Web 前端】jQuery 里的 each() 是什么函数?你是如何使用它的?
【5月更文挑战第2天】【Web 前端】jQuery 里的 each() 是什么函数?你是如何使用它的?
|
5天前
|
存储 前端开发 JavaScript
【Web 前端】如何找到所有 HTML select 标签的选中项?
【5月更文挑战第2天】【Web 前端】如何找到所有 HTML select 标签的选中项?
|
5天前
|
JavaScript 前端开发 C++
【Web 前端】JavaScript window.onload 事件和 jQuery ready 函数有何不同?
【5月更文挑战第2天】【Web 前端】JavaScript window.onload 事件和 jQuery ready 函数有何不同?
|
5天前
|
前端开发 JavaScript
【Web 前端】$(document).ready() 是个什么函数?为什么要用它?
【5月更文挑战第2天】【Web 前端】$(document).ready() 是个什么函数?为什么要用它?
|
5天前
|
JavaScript 前端开发
【Web 前端】如何在点击一个按钮时使用 jQuery 隐藏一个图片?
【5月更文挑战第2天】【Web 前端】如何在点击一个按钮时使用 jQuery 隐藏一个图片?
|
6天前
|
JavaScript 前端开发
【Web 前端】 jQuery 里的 ID 选择器和 class 选择器有何不同?
【5月更文挑战第1天】【Web 前端】 jQuery 里的 ID 选择器和 class 选择器有何不同?
|
6天前
|
JavaScript 前端开发
【Web 前端】网页上有 5 个div元素,如何使用JQ来选择它们?
【5月更文挑战第1天】【Web 前端】网页上有 5 个div元素,如何使用JQ来选择它们?
|
6天前
|
JavaScript 前端开发
【Web 前端】jQuery 库中的 $() 是什么?
【5月更文挑战第1天】【Web 前端】jQuery 库中的 $() 是什么?
|
6天前
|
前端开发 JavaScript UED
【Web 前端】异步函数
【5月更文挑战第1天】【Web 前端】异步函数