webview方式嵌套H5应用加载慢解决方案
一、前言
uni-app
项目中通过webview
方式嵌套H5应用时,出现页面加载慢的用户体验问题。尤其当应用第一次加载H5应用时,页面白屏大致有3-4s!
经过分析发现,h5页面第一次加载时会下载页面静态资源(包括图片、字体库文件、css样式文件、js脚本等),后续加载时浏览器引擎在缓存机制的作用下会直接加载缓存信息,渲染较快。
- 页面初次渲染
其中,
finish
:页面最后一个请求截止的时间,如果页面加载完成后,触发了ajax请求,那么该时间会变更。DOMContentLoaded
:dom内容加载并解析完成的时间,即页面白屏时间。load
:页面所有的资源(图片、音频、视频等)加载完成的时间。
老生常谈,从输入URL到页面展示,发生了什么?
- 首先从本地查找域名,有的话直接用hosts文件里的ip地址,否则查询DNS,得到ip地址;
- 建立TCP连接——进行所谓的“三次握手”;
- 客户端发送http请求;
- 服务端处理,并返回结果给客户端;
- 关闭TCP连接——需要“四次挥手”;
- 浏览器收到结果,开始解析资源(
JS、CSS、HTML
),解析HTML
生成的dom
树,和同时解析css
生成的cssom
树结合生成渲染树;
我们可以在控制台输入window.performance.getEntriesByType('paint')
来获取 First Paint
(FP:文档中任意元素首次渲染时间)和 First Contentful Paint
(FCP:也就是我们常说的白屏时间 )
当然了,这两个值也不是固定的,比如在第一次打开页面和第二次打开页面时是不同的。
二、优化建议
-
使用自定义组件模式
使用自定义组件模式,在 manifest 中配置自定义组件模式(HBuilderX1.9起新建项目默认即为自定义组件模式)。在复杂页面中,页面中嵌套大量组件,如果是非自定义组件模式,更新一个组件会导致整个页面数据更新。而自定义组件模式则可以单独更新一个组件的数据。
在App端,除了上述好处,自定义组件模式还新增了一个独立的js引擎,加快启动速度、减少js阻塞。
之前的非自定义组件模式已经不再推荐,如果你的应用还是非自定义组模式,请尽快升级。
-
避免使用大图
页面中若大量使用大图资源,会造成页面切换的卡顿,导致系统内存升高,甚至白屏崩溃。尤其是不要把多张大图缩小后显示在一个屏幕内,比如上传图片前选了数张几M体积的照片,然后缩小在一个屏幕中展示多张几M的大图,非常容易白屏崩溃。
推荐通过阿里云oss,来压缩图片处理。
-
优化数据更新
在 uni-app 中,定义在 data 里面的数据每次变化时都会通知视图层重新渲染页面。 所以如果不是视图所需要的变量,可以不定义在 data 中,可在外部定义变量或直接挂载在vue实例上,以避免造成资源浪费。 -
长列表
长列表中如果每个item有一个点赞按钮,点击后点赞数字+1,此时点赞组件必须是一个单独引用的组件,才能做到差量数据更新。否则会造成整个列表数据重载。(要求自定义组件模式)长列表中每个item并不一定需要做成组件,取决于你的业务中是否需要差量更新某一行item的数据,如没有此类需求则不应该引入大量组件。(点击item后背景变色,属于css调整,没有更新data数据和渲染,不涉及这个问题)
app端
nvue
的长列表应该使用list组件,有自动的渲染资源回收机制。vue页面使用页面滚动的性能,好于使用scroll-view
的区域滚动。如需要左右滑动的长列表,请参考“在HBuilderX新建uni-app项目” 的 新闻模板,那是一个标杆实现。自己用
swiper
和scroll-view
做很容易引发性能问题。 -
减少一次性渲染的节点数量
页面初始化时,逻辑层如果一次性向视图层传递很大的数据,使视图层一次性渲染大量节点,可能造成通讯变慢、页面切换卡顿,所以建议以局部更新页面的方式渲染页面。如:服务端返回100条数据,可进行分批加载,一次加载50条,500ms 后进行下一次加载。
-
减少节点嵌套层级
深层嵌套的节点在页面初始化构建时往往需要更多的内存占用,并且在遍历节点时也会更慢些,所以建议减少深层的节点嵌套。 -
避免视图层和逻辑层频繁进行通讯
减少scroll-view
组件的 scroll 事件监听,当监听scroll-view
的滚动事件时,视图层会频繁的向逻辑层发送数据;监听
scroll-view
组件的滚动事件时,不要实时的改变scroll-top/scroll-left
属性,因为监听滚动时,视图层向逻辑层通讯,改变scroll-top/scroll-left
时,逻辑层又向视图层通讯,这样就可能造成通讯卡顿。注意
onPageScroll
的使用,onPageScroll
进行监听时,视图层会频繁的向逻辑层发送数据; 多使用css动画,而不是通过js的定时器操作界面做动画。 -
优化页面切换动画
页面初始化时若存在大量图片或原生组件渲染和大量数据通讯,会发生新页面渲染和窗体进入动画抢资源,造成页面切换卡顿、掉帧。建议延时100ms~300ms渲染图片或复杂原生组件,分批进行数据通讯,以减少一次性渲染的节点数量。App端动画效果可以自定义。
popin/popout
的双窗体联动挤压动画效果对资源的消耗更大,如果动画期间页面里在执行耗时的js,可能会造成动画掉帧。此时可以使用消耗资源更小的动画效果,比如slide-in-right/slide-out-right
。 -
优化样式渲染速度
如果页面背景是深色,在vue
页面中可能会发生新窗体刚开始动画时是灰白色背景,动画结束时才变为深色背景,造成闪屏。这是因为webview
的背景生效太慢的问题。此时需将样式写在App.vue
里,可以加速页面样式渲染速度。App.vue
里面的样式是全局样式,每次新开页面会优先加载App.vue
里面的样式,然后加载普通vue
页面的样式。另外nvue
页面不存在此问题,也可以更改为nvue
页面。 -
使用 nvue 代替 vue
在 App 端 uni-app 的 nvue 页面是基于 weex 定制的原生渲染引擎,实现了页面原生渲染能力、提高了页面流畅性。若对页面性能要求较高可以使用此方式开发。
优化App启动速度注意事项
-
工程代码越多,包括背景图和本地字体文件越大,对App的启动速度有影响,应注意控制体积。组件引用的前景图不影响性能。
-
App端的 splash 关闭有白屏检测机制,如果首页一直白屏或首页本身就是一个空的中转页面,可能会造成 splash 10秒才关闭。
-
App端使用自定义组件模式时启动速度更快,首页为nvue页面时启动速度更快。
-
App设置为纯nvue项目(manifest里设置app-plus下的renderer:“native”),这种项目的启动速度更快,2秒即可完成启动。因为它整个应用都使用原生渲染,不加载基于webview的那套框架。
优化包体积
-
uni-app发行到小程序时,自带引擎只有几十K,主要是一个定制过的vue.js核心库。如果使用了es6转es5、css对齐的功能,可能会增大代码体积,可以配置这些编译功能是否开启。
-
uni-app的H5端,自带了
vue.js
、vue-rooter
及部分es6 polyfill
库,这部分的体积gzip后只有92k,和web开发使用vue基本一致。而内置组件ui库(如picker
、switch
等)、小程序的对齐js api等,相当于一个完善的大型ui库。但大多数应用不会用到所有内置组件和API。由此uni-app提供了摇树优化机制,未摇树优化前的uni-app整体包体积约500k,服务器部署gzip后162k。开启摇树优化需在manifest.json
配置。 -
uni-app的App端,因为自带了一个独立v8引擎和小程序框架,所以比HTML5Plus或mui等普通hybrid的App引擎体积要大。Android基础引擎约15M。App还提供了扩展模块,比如地图、蓝牙等,打包时如不需要这些模块,可以裁剪掉,以缩小发行包体积。在
manifest.json-App模块权限
里可以选择。 -
App端支持如果选择纯nvue项目(manifest里设置
app-plus
下的renderer:"native"
),包体积可以进一步减少2M左右。 -
uni-app的App端默认包含arm32和x86两个cpu的支持so库。这会增大包体积。如果你在意体积控制,可以在manifest里去掉x86 cpu的支持(manifest可视化界面-App其他设置里选择cpu),这可以减少包体积到9M。但代价是不支持intel的cpu了。一般手机都是arm的,仅个别少见的Android pad使用x86 cpu。另外as的模拟器里如果选择x86时也无法运行这种apk。
三、实施方案
从以下优化点进行页面优化:
- 优化项目结构,减小项目组包体积;
- 去除项目冗余字体文件;
- 启动应用服务器端gzip压缩;
3.1 优化项目结构,减小项目组包体积
3.2 去除项目冗余字体文件
由上图可知,项目中字体库文件大小为6.8M,那么该文件中具体包含哪些字体呢?是不是全部为项目中需要的字体文件呢?是否存在冗余字体文件?
3.2.1 字体筛选
解决方案:通过识别常用5000汉字,将原有字体文件通过取子集、不压缩的方式获得小体积字体文件。压缩后的字体文件体量为原有字体库文件的 7% 左右!信息如下:
将大体量字体库文件经过常用5000汉字筛选后生成的字体库文件后,页面加载效果如下:
实验结果:通过对比前后页面渲染效果,可知渲染时长缩短至原有的 32% 左右,页面白屏时间缩短至原有项目的 25% 左右,渲染性能大幅提升!以上字体文件仅是通过常用汉字取子集的方式生成的,还未利用压缩技术,再经过压缩后,相信字体库文件体量会大幅减小,页面渲染性能会再上一个新台阶!
3.2.2 字体压缩
通过3.2.1小节给出的常用汉字筛选,实践发现常用5000字居然不包括平时常用的汉字,例如:综!导致页面字体渲染结果不一致!
为了解决该问题,要么扩大汉字筛选范围(无统一标准,仍存在遗漏风险);要么不适用字体筛选策略!
通过不识别常用5000汉字,将原有字体文件只通过压缩的方式获得小体积字体文件。压缩后的字体文件体量为原有字体库文件的 67.6% 左右!信息如下:
将大体量字体库文件经过压缩生成的字体库文件后,页面加载效果如下:
实验结果:通过对比前后页面渲染效果,可知渲染时长缩短至原有的 73% 左右,页面白屏时间缩短至原有项目的 48.5% 左右,渲染性能仅小幅提升!
此外,文件压缩方式也可以采用字蛛实现。
有关字体库文件压缩,详参博文《跨平台应用开发进阶(四十七)大体量字体库文件处理方案》。
3.3 gzip压缩
先来了解一下GZIP ,gzip是GNU zip的缩写,它是一个GNU自由软件的文件压缩程序,也经常用来表示gzip这种文件格式。软件的作者是Jean-loup Gailly和Mark Adler。1992年10月31日第一次公开发布,版本号是0.1,目前的稳定版本是1.2.4。
gzip压缩流程
- Web服务器接收到浏览器的HTTP请求后,检查浏览器是否支持HTTP压缩(Accept-Encoding 信息);
- 如果浏览器支持HTTP压缩,Web服务器检查请求文件的后缀名;
- 如果请求文件是HTML、CSS等静态文件,Web服务器到压缩缓冲目录中检查是否已经存在请求文件的最新压缩文件;
- 如果请求文件的压缩文件不存在,Web服务器向浏览器返回未压缩的请求文件,并在压缩缓冲目录中存放请求文件的压缩文件;
- 如果请求文件的最新压缩文件已经存在,则直接返回请求文件的压缩文件;
- 如果请求文件是动态文件,Web服务器动态压缩内容并返回浏览器,压缩内容不存放到压缩缓存目录中。
gzip
可以级大的加速网站.有时压缩比率高到80%,近来测试了一下,最少都有40%以上,还是相当不错的。在Apache2之后的版本,模块名不叫gzip
,而叫mod_deflate
。
启用Gzip
压缩功能, 可以使网站的css、js 、xml、html
等静态资源在传输时进行压缩,经过Gzip
压缩后资源可以变为原来的30%甚至更小,尽管这样会消耗一定的cpu资源,但是会节约大量的出口带宽来提高访问速度。
Gzip
的压缩页面需要浏览器和服务器双方都支持,实际上就是服务器端压缩,传到浏览器后解压并解析。浏览器那里不需要我们担心,因为目前的大多数浏览器都支持解析Gzip
。
注意⚠️:不建议压缩图片和大文件:图片如jpg、png文件本身就会有压缩,所以就算开启gzip
后,压缩前和压缩后大小没有多大区别,所以开启了反而会白白的浪费CPU资源。(如果优化可以可以图片的生命周期设置长一点,让客户端来缓存)
而大文件资源会消耗大量的cpu资源,且不一定有明显的效果。
3.3.1 nginx 配置gzip
在 nginx.conf
(Nginx
配置文件)中,在http
块内或者在单个server
块里添加后重启nginx
。
./nginx -s reload
#开启gzip
gzip on;
# 检查是否存在请求静态文件的gz结尾的文件,如果有则直接返回该gz文件内容,不存在则先压缩再返回
gzip_static off;
#低于1kb的资源不压缩
gzip_min_length 1k;
#压缩级别1-9,越大压缩率越高,同时消耗cpu资源也越多,建议设置在5左右。
gzip_comp_level 5;
#需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片.
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css application/octet-stream;
#配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
gzip_disable "MSIE [1-6]\.";
#是否添加“Vary: Accept-Encoding”响应头
gzip_vary on;
其中,待压缩的资源类型可以从控制台Content-Type
看到:
验证gzip开启
按1-2次 ctrl+f5
, 以强制刷新的方式浏览页面,然后在network面板查看静态资源的响应头,若包含content-encoding: gzip
则表示经过gzip
。
页面渲染效果如下:
由开启gzip
之后的页面渲染时间,可知页面渲染完成时间提升了80%,白屏时间缩短至1s内!
3.3.2 Apache 配置gzip
3.3.2.1 使用 mod_deflate 模块
mod_deflate
是Apache的一个内置模块,可以用来压缩传输内容。可以通过以下步骤来启用它。
打开Apache的配置文件httpd.conf
。
查找以下行并取消注释它:
LoadModule deflate_module modules/mod_deflate.so
在http.conf
文件最末尾添加gzip
压缩配置的代码:
# GZIP压缩模块配置
<IfModule mod_deflate.c>
# 告诉apache对传输到浏览器的内容进行压缩
SetOutputFilter DEFLATE
# 压缩等级 9
DeflateCompressionLevel 9
# 设置不对后缀gif,jpg,jpeg,png,exe等文件进行压缩
SetEnvIfNoCase Request_URI .(?:gif|jpe?g|png|exe|t?gz|zip|bz2|sit|rar|pdf|mov|avi|mp3|mp4|rm)$ no-gzip dont-vary
# 压缩类型 html、xml、php、css、js
AddOutputFilterByType DEFLATE text/html text/plain text/xml application/x-httpd-php text/css application/x-javascript
AddOutputFilter DEFLATE js css
# Netscape 4.x 有一些问题,所以只压缩文件类型是text/html的
BrowserMatch ^Mozilla/4 gzip-only-text/html
# Netscape 4.06-4.08 有更多的问题,所以不开启压缩
BrowserMatch ^Mozilla/4.0[678] no-gzip
# IE浏览器会伪装成 Netscape ,但是事实上它没有问题
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
</IfModule>
里面的文件MIME
类型可以根据自己情况添加,至于PDF 、图片、音乐文档之类的这些文件本身都已经高度压缩格式,重复压缩的作用不大,反而可能会因为增加CPU的处理时间及浏览器的渲染问题而降低性能。所以就没必要对其进行Gzip压缩。
其中,DeflateCompressionLevel
用来设置压缩比率,取值范围在 1(最低) 到 9(最高)之间,不建议设置太高,虽然有很高的压缩率,但是占用更多的CPU资源。
接下来,对指定的文件配置缓存的生存时间,去除mod_headers.so
模块前面的“#”号注释:
LoadModule headers_module modules/mod_headers.so
接着在http.conf最末尾添加文件缓存时间配置的代码:
# 文件缓存时间配置
<FilesMatch ".(flv|gif|jpg|jpeg|png|ico|swf|js|css)$">
Header set Cache-Control "max-age=2592000"
</FilesMatch>
3、配置完成后,重启Apache服务即可。
3.3.2.2 使用 mod_gzip 模块
mod_gzip
是Apache的第三方模块,可以用于开启gzip压缩。可以按照以下步骤启用它。
下载和编译mod_gzip
模块。将以下代码添加到httpd.conf
文件中:
LoadModule gzip_module modules/mod_gzip.so
在<VirtualHost>
段中添加以下代码:
<Location /> # 对所有类型的文件启用gzip压缩 SetOutputFilter GZIP # 配置要压缩的文件类型 mod_mime_add_encoding_type application/x-msdownload .exe mod_mime_add_encoding_type application/pdf .pdf mod_mime_add_encoding_type application/zip .zip mod_mime_add_encoding_type text/html .html mod_mime_add_encoding_type text/plain .txt # 压缩级别 G
3.4 ETag
gzip
属于HTTP
协议的内容。其缺点就是重新渲染的问题!
ETag
全称EntityTags
,HTTP
协议规格说明中定义“ETag
”为“被请求变量的实体值”。
也可以把ETag
理解为是一个客户端与服务器间的关联标记。这个记号告诉客户端,当前网页在上次请求之后是否有发生变化,当发生变化时,ETag
的值重新计算,并返回200
状态码。如果没有变化,返回304
状态码。从而不会重新加载整个页面信息。
这样不如使用 etag
,Nginx
开启etag
只需要在server
配置( nginx.conf
在http
块内)里加上一行:etag on;
即可。
注意⚠️:
Nginx
版本1.3.3
以下,不支持ETag
!- 开启
gzip
时,可能与etag
出现冲突,用浏览器多次请求此网站的静态元素,如果只返回200,不返回304,证明存在冲突,需要关闭gzip
解决冲突,即将上一步中的gzip on;
改为gzip off;
3.5 http/2
现在http2
已经席卷而来,而且其有一个强大的优势,在于对于一个域只进行一次tcp
连接,使用多路复用,传输多个资源(同时加载),这样就不必使用诸如雪碧图、合并css/js
文件等技术减少请求数了(使用雪碧图只有一个优点:减少请求次数,这和它不可避免的缺点(高清屏会失真、图片变化极不方便)相比,简直不足为道)。
这个技术的使用也很简单,只需要使用nginx 1.10.0和openssl 1.0.2以上版本,安装好后再配置文件中( ngnix.conf
,写在http
中的server
块中 )加上:listen 443 ssl http2;
即可。
当然,对于不兼容HTTP2
的浏览器,nginx
也会自动处理。
四、拓展阅读
- 点赞
- 收藏
- 关注作者
评论(0)