性能优化
H5的性能优化有一个很重要的指标——首屏加载速度。而我们一般所说的H5性能优化指的也是如何提升首屏加载速度。而我们常用的优化首屏速度的方案,可以大致总结为以下几种:
- 首屏资源懒加载,延迟加载
- 首屏资源gzip和cdn加速
- 减少js包大小
- 图片裁剪或压缩
具体可以参考文章,一般优化后都能有不错的效果 。
随着我们项目用户数越来越多,对性能要求也越来越极致。为此,我想到了一些其他的方案。
一、页面数据本地缓存
用户的网络情况是是否复杂的,经常还会遇到弱网情况。可以将页面的数据缓存到本地,用户下次打开时无需网络请求就可以直接显示了。这可以大大加快用户看到页面内容的速度。
浏览器的本地缓存方案大致有以下几种:
- localStorage:这是前端开发使用最多的一种缓存之一。可以用作持久化缓存。读写是同步,数据大时会阻塞主线程。只能存储string类型,存储容量只有2.5M到10M之间(不同浏览器不同)。
- Web SQL:前端数据库。但它兼容性不好,自测只有chrome浏览器支持,ie或微信内置浏览器都是不支持的。
- IndexedDB:浏览器提供的本地数据库。 特点:
- 键值对存储,简单易用。
- 读写异步。访问数据库不会阻塞调用线程。
- 存储空间大。一般来说不少于 250MB,甚至没有上限。
- 支持事务,意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 支持二进制存储,可以直接存储对象。
- 具有同源策略,安全。
- 各浏览器兼容性不错,自测微信和QQ自带浏览器都是支持的。
于是我选择IndexedDB
作为我们H5的缓存方案。
实现核心代码:
createOrOpenDb({ dbName, storeName, keyPath, indexOptions }) {
const mIndexedDB = window.indexedDB || window.webkitIndexedDB
|| window.mozIndexedDB;
const dbOpenRequest = mIndexedDB.open(dbName, this.dbVersion);
return new Promise((resolve, reject) => {
dbOpenRequest.onsuccess = (event) => {
console.log(`${dbName} 打开或创建成功`);
this.db = event.target.result;
resolve(this.db);
};
dbOpenRequest.onerror = (event) => {
reject(event.target.error.message);
};
dbOpenRequest.onupgradeneeded = (event) => {
console.log(`${dbName} upgrade version : ${this.dbVersion}`);
this.db = event.target.result;
this.createObjectStore(storeName, keyPath, indexOptions);
};
});
}
createObjectStore(storeName, keyPath, indexOptions) {
if (!this.db.objectStoreNames.contains(storeName)) {
const objectStore = this.db.createObjectStore(storeName, {
keyPath,
});
objectStore.createIndex(keyPath, keyPath, {
unique: true,
});
for (const prop of indexOptions) {
objectStore.createIndex(prop, prop, {
unique: false,
});
}
console.log(`${storeName} 数据结构定义完毕`);
}
},
二、JS预加载
我们H5项目使用的是Vue框架,比较复杂,存在大量多级页面。跳转到二级页面时,需要网络加载二级页面的js文件。为了提升页面间跳转的速度,我想到了是否可以在当前页面提前加载下一级页面的js文件?这样跳转到二级页面时,无需先请求js文件,可以直接显示。于是,我便开始了js的预加载方案的预研:
Web Worker预加载js
我们知道JavaScript
是单线程的,js的加载会阻塞主线程,影响页面渲染(这句话其实表达不准确,后面会讲)。必须保证在不影响主线程的情况下,对下一级页面的js进行预加载,就必须用到多线程。于是我便想到了Web Worker
。
Web Worker
为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。
可在我实现的过程中,遇到了一个棘手的问题。new Worker()
参数必须是来自网络且与主线程同源,所以需要提前知道每个页面webpack
打包后对应的js文件路径。也就是我需要在编写Worker代码时就要知道代码打包后的路径,并且页面每次打包生成的js文件名都有随机数的。这对我们编写和维护Web Worker
文件造成了很大影响。
在我苦思如何解决这个问题时,我无意中发现了另外一个简单的js预加载方案。
import预加载页面
在Vue中,我们可以直接调用import()
加载js文件或者Vue组件。但是因为我前面提到的“js的加载会阻塞主线程,影响页面渲染”,所以我一开始想到要用Web Worker
多线程能力来实现。但这句话表达是不准确的。经过我试验,网络加载js文件并不会阻塞主线程,只有js代码的执行才会阻塞。为什么会有“js的加载会阻塞主线程”这个结论,我想可能是因为之前的网页大多数都是在html添加<script>
标签加载js,一般js文件加载即执行。
在Vue中,调用import('src/xxx/index.vue')
只会网络请求了该Vue组件,并不会有js代码的执行,因此也并不会阻塞主线程。有了这个结论,js的预加载方案就很容易实现了。方案可以查看下面的流程图,具体代码也是比较简单的。
优化效果
经过上面两点优化后,页面的启动耗时和转场时间都有了很大的提升。
体验优化
H5性能优化是为了用户更快的看到页面和交互,而H5的体验优化,则是为了用户在使用过程中有爽
的体验。我们知道,H5的体验一般不如原生,其中一个很大的原因是H5在交互和动效方面与原生app有很大的差距。而接下来我对H5的体验优化,整体的方向就是仿App体验。
一、加载过的页面重新展示时不用重复加载(keep-alive)
Vue是单页面应用,默认情况下回退上一页是会重新加载页面的,这大大影响了页面间跳转体验。不过Vue提供了keep-alive
模式,可以让控制任意组件在router-view
中不被销毁,可以实现原生App那种丝滑回退的感觉。实现keep-alive
一般有两种方式:
1. include exclude
可以在router-view
中添加include
或者exclude
属性,设置哪些组件需要keep-alive
。参数是组件的name。
- include: 字符串或正则表达式。只有匹配的组件会被缓存。
- exclude: 字符串或正则表达式。任何匹配的组件都不会被缓存。
<keep-alive include="Index,MatchIndex,TeamHome,MatchDetailIndex">
<router-view/>
</keep-alive>
2. 不同的router-view
使用$route.meta
的keepAlive
属性:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
需要在router中设置router的meta:
routes: [
{
path: '/',
name: 'Index',
component: Index,
meta: {
keepAlive: true // 需要缓存
}
},
{
path: '/page1',
name: 'Page1',
component: Page1,
meta: {
keepAlive: false // 不需要被缓存
}
}
]
实现后Vue生命周期方法会回调activated
和deactivated
:
- activated:Vue组件处于前台时会回调该方法。可以做数据的处理。
- deactivated:Vue组件立刻前台时回回调该方法。可做一些资源是释放。
实现效果:
优化前:
优化后:
二、列表滚动位置记录
H5另外一个体验不好的地方,点击列表跳转后,回退到上一级页面,并不会记录滚动的位置。不过,页面实现了keep-alive后,要想实现列表滚动位置记录就比较简单了。
可以在router.afterEach()
时把页面滑动距离存在meta
中,然后页面activated
时取出之前存的滑动距离重新设置上去。
function afterEachCb(to, from) {
const keepPositionWrap = document.querySelector('.keep-position-wrap');
if (!from.meta || !from.meta.keepPosition) {
return;
}
if (keepPositionWrap) {
const { scrollTop } = keepPositionWrap;
from.meta.$scrollTop = scrollTop;
}
}
activated() {
const keepPositionWrap = document.querySelector('.keep-position-wrap');
if (!this.$route || !this.$route.meta || !this.$route.meta.keepPosition) {
return;
}
if (keepPositionWrap && this.$route.meta.$scrollTop && typeof keepPositionWrap.scrollTo === 'function'
) {
keepPositionWrap.scrollTo(0, this.$route.meta.$scrollTop);
}
}
优化前:
优化后:
三、页面转场动画
原生App页面间的转场是有切换动画的,让用户在页面间跳转显得更顺畅和自然。那H5是否也能实现这种效果呢?
于是我查找方案时,留意到GitHub上有一个Star比较多的库,能在Vue中很好的实现页面间的转场动画。Vueg----page-transition-plugin,具体使用和原理,有兴趣可以直接去Github查看。接入后,效果如下:
优化前:
优化后:
以上,是我对H5的性能优化与体验优化总结。
END