引言

website-speed-3seconds

在用户体验方面,网站的加载时间是决定成功的因素之一。各种研究的结果表明:

  • 将近50%的互联网用户期望网页加载时间少于2秒。如果是在3秒后加载页面,则很大一部分访问者会离开该网站。
  • 同样,将近80%的用户表示会避免访问曾经体验差的一些网址。将网页加载时间延迟一秒会使客户满意度降低约16%。例如,谷歌在2006年进行了试验,显示的搜索结果数量从10增加到30,加载时间仅增加了半秒,但是结果确是流量和收入下降了约20%。

那么我们需要做些什么来留住用户呢?

背景

618大促活动主要有两个活动,会场类活动和互动类活动。

会场类活动通常是一个庞大的页面,包含大量的数据请求和图片渲染,如果我们在页面初始化时就将所有数据请求回来并进行楼层渲染,页面加载速度会非常慢,但其实用户有时并不会滚动去看下面的内容, 这样就造成了非首屏部分的渲染, 所以我们需要进行首屏渲染优化。

互动类活动通常是大屏游戏,包含大量图片和动效,如果未加载完成就展示会影响动效展示效果,所以通常我们会进行预加载处理。

预加载处理

首先我们先了解下 什么是预加载技术:

WEB中的预加载就是在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验,减少等待的时间。否则,如果一个页面的内容过于庞大,没有使用预加载技术的页面就会长时间的展现为一片空白,直到所有内容加载完毕。预加载通常分为三大类,分别为菊花图类、进度条类、skeleton screen(加载占位图)。

1. 菊花图类

首先我们来看菊花图类,此类loading方式主要适用于会场类活动,页面首屏数据多,loading页面主要是用于数据加载和简单的头图背景加载。我们创建一个loading的 component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="loading">
<svg width="50" height="50" viewBox="0 0 28 28">
<circle
class="page__loading--circle"
cx="14"
cy="14"
r="12"
stroke="currentColor"
stroke-width="2"
fill="none"
stroke-dasharray="76"
stroke-dashoffset="76"
stroke-linecap="round"
/>
</svg>
</div>

然后在主页面定义三个标识变量loadDataloadImgloaded 初始化为false,当首屏数据请求成功后我们将loadData置为true,当首屏图片加载完成则将loadImg置为true,最后这两个值都为true后将loaded置为true。这样我们会在数据请求成功并且首屏图片加载完成后才进入页面,但假如用户网络情况很差如果一直处于loading页面也会影响用户体验,所以我们还需要增加一个定时器,当超过3s则将这三个标识统一置为true,直接进入页面,避免过长等待。

2. 进度条类

当我们开发视图/图片较多的活动页面,或者是需要逐帧图片来完成的动画效果时,最好都要做预加载。由于明确知道交互所需时间,或者知道一个大概值的时候,我们一般会选择进度条类加载页,进度条的优势在于用户知道还需要等待的时间,安抚用户,让其耐心等待。

那么,哪些资源才需要预加载呢?
web中包含的资源有很多种,图片、音视频之类的媒体文件,另外就是js、css文件,这些都需要发送请求来获取。那这些资源难道我们都预加载?
当然不是了,一般来说,js、css文件不需进行预加载。脚手架都会进行压缩合并,将请求数降到最低,最终只有一个文件,所以并不需要预加载。
那么需要预加载的就是媒体文件了,图片、音视频之类。这类资源也得根据实际情况来选择哪些需要预加载。比如大多数本地图片就需要预加载,而由接口动态获取的图片则无法预加载,也无需做预加载处理。用作音效、小动画的音视频可以预加载。

以上是预加载的一些理论知识,接下来让我们从技术角度出发,来实现预加载。原理其实也相当简单,就是维护一个资源列表,挨个去加载列表中的资源,然后在每个资源加载完成的回调函数中更新进度即可。以图片为例,大致的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import imgUrl1 from '@/assets/header1.png';
import imgUrl2 from '@/assets/header2.png';
import imgUrl3 from '@/assets/header3.png';
// ......其他图片资源

data() {
return {
loaded: false, // 加载状态
currentIndex: 0, // 当前加载的索引
total: 0, // 资源总数
progress: 0, // 加载进度
resources: [imgUrl1,imgUrl2,imgUrl3] // 资源数组
};
},
methods: {
loadResources() {
let vm = this;
this.total = this.resources.length || 0;
this.resources.forEach(function (url) {
var image = new Image();
image.onload = function () {
vm.loadImg()
}
image.onerror = function () {
vm.loadImg()
console.log('加载失败:' + url);
}
image.src = url;
});
},
loadImg() {
++this.currentIndex;
this.progress = this.currentIndex/this.total;
if (this.currentIndex == this.total) {
this.loaded = true
}
}
}

关于加载进度,上面获取到的进度其实是离散的点,不是连续的。其实利用HTML5的xhr2的新特性我们也可以尝试获取更加精确的进度。我们可以通过onprogress事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的onprogress事件:

  • 上传触发的是xhr.upload对象的 onprogress事件
  • 下载触发的是xhr对象的onprogress事件

以加载视频为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const xhr = new XMLHttpRequest();
xhr.open(
"GET", "//vod.300hu.com/4c1f7a6atransbjngwcloud1oss/7690c6f6173824224820482049/v.f20.mp4?dockingId=f61cef26-7193-4f9b-a744-9093472fb4dc&storageSource=3"
);
xhr.responseType = "blob";
xhr.onprogress = function(e) {
if (!ignoreAjaxProgress) {
self.setState({ progress: Math.floor((e.loaded / e.total) * 100) });
}
};
xhr.onload = function() {
if (!ignoreAjaxProgress) {
self.setState({ screenNumber: 2 });
}
};
xhr.send(null);

// 8秒内AJAX还没有加载完视频,则直接模拟假的进度条到100%

function fakeProgress() {
if (self.state.progress >= 95) {
self.setState({
screenNumber: 2
});
} else {
self.setState({
progress: self.state.progress + 1 + Math.floor(2 * Math.random())
});
setTimeout(fakeProgress, 40);
}
}

setTimeout(() => {
if (this.state.screenNumber === 1) {
ignoreAjaxProgress = true;
fakeProgress();
}
}, 8000);
}
```
<img src="https://img10.360buyimg.com/imagetools/jfs/t1/122803/29/4564/167286/5ee0d0acE802785ed/b5ee77040b6a3811.gif" width="375" /> <img src="https://img12.360buyimg.com/imagetools/jfs/t1/114685/12/9853/74576/5ee0cf6aE658ac1a7/b85c6df83eb45517.gif" width="375" />

#### 3.skeleton screen
以上两种方式只能告诉用户加载还需要一定的时间,既无法让用户感知到页面加载得更快,也无法给用户一个焦点,让用户将关注集中到这个焦点上,此时我们就需要骨架屏。

在最开始关于 MIT 2014 年的研究中已有提到,用户大概会在 200ms 内获取到界面的具体关注点,在数据获取或页面加载完成之前,给用户首先展现骨架屏,骨架屏的样式、布局和真实数据渲染的页面保持一致,这样用户在骨架屏中获取到关注点,并能够预知页面什么地方将要展示文字什么地方展示图片,这样也就能够将关注焦点移到感兴趣的位置。当真实数据获取后,用真实数据渲染的页面替换骨架屏,如果整个过程在 1s 以内,用户几乎感知不到数据的加载过程和最终渲染的页面替换骨架屏,而在用户的感知上,出现骨架屏那一刻数据已经获取到了,而后只是数据渐进式的渲染出来。这样用户感知页面加载更快了。
<img src="https://img13.360buyimg.com/imagetools/jfs/t1/119774/37/8491/3649/5ee0db3dEebb7362f/793268bce935a427.gif" width="375" />

骨架屏适用一些固定位置元素,不适用于一些动态位置元素。接下来看下具体实现思路:
通过分析vue中页面的加载过程,我发现当app.js执行完之后,会用vue渲染成的dom将index.html中的div#root完全替换掉。基于以上原理,其实只要将骨架屏内容直接插入div#app中即可实现骨架屏。

``` javascript
<div id="app">
<div class="skeleton page">
<div class="skeleton-nav"></div>
<div class="skeleton-swiper"></div>
<ul class="skeleton-tabs">
<li v-for="i in 8" class="skeleton-tabs-item"><span></span></li>
</ul>
<div class="skeleton-banner"></div>
<div v-for="i in 6" class="skeleton-productions"></div>
</div>
</div>

上面这种虽然简单有效,但是确不易扩展,考虑到是在Vue项目里,自然希望骨架屏也是一个.vue文件,它能够在构建时由工具自动注入到div#app里面。所以我们引入vue-skeleton-webpack-plugin插件。

首先创建一个Skeleton.vue文件,将上面的css和html导入其中,然后再创建一个入口文件entry-skeleton.js:

1
2
3
4
5
6
7
8
9
import Vue from 'vue'
import Skeleton from './Skeleton.vue'

export default new Vue({
components: {
Skeleton
},
template: '<skeleton />'
})

之后就是配置webpack.dev.conf.js和webpack.prod.conf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
function resolve(dir) {
return path.join(__dirname, '..', dir)
}

plugins: [
new SkeletonWebpackPlugin({
webpackConfig: {
entry: {
app: resolve('./src/entry-skeleton.js')
}
}
})
]

最后我们正常启动项目就可以了,调试的时候可以在浏览器中修改network为Low-end mobile这样加载的时间就比较长。让我们开看下效果:

懒加载处理

在说完了预加载后,我们再来看懒加载,首先看下懒加载的定义:

懒加载(Load On Demand)是一种独特而又强大的数据获取方法,它能够在用户滚动页面的时候自动获取更多的数据,而新得到的数据不会影响原有数据的显示,同时最大程度上减少服务器端的资源耗用。

两种技术本质上是行为相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。目前使用到的主要是数据分屏加载、楼层懒加载和图片懒加载。

数据分屏加载

实现原理:先加载一部分数据,当触发某个条件时加载剩余的数据,新得到的数据不会影响原有数据的显示,同时最大幅度的减少服务器端资源耗用。
基于以上原理,主要有三种实现方式:

  1. 第一种是纯粹的延迟加载,使用setTimeOut和setInterval进行加载延迟。
  2. 第二种是条件加载,符合某种条件,或是触发某些事件才开始异步加载。
  3. 第三种是可视区加载,仅记载用户的可视区域,这个主要监控滚动条来实现,一般会距用户看到某些图片前的一段距离时开始进行记载,这样就可保证用户拉下时正好可以看到加载完毕后的图片或是内容。

第一种方式最简单但是还是会存在一定服务器端资源浪费,延迟加载的内容可能用户并没有实际滑动到展示区域,所以我采用第三种方式,也就是数据分屏加载,vue中监测滚动事件进行数据分屏加载,以会场活动为例:
根据页面数据请求情况将页面分屏数,此处以四屏为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
data() {
return {
firstScreenLoad: false, // 首屏加载是否完成
loadFloor: ['header','dapai','sence','brand'],
floorLoaded: [] // 已加载楼层
};
},
created() {
this.floorLoaded = Array.from({length: this.loadFloor.length}, () => false)
},
methods: {
loadData() {
window.addEventListener('scroll', () => {
if (!this.firstScreenLoad) return false

let top = (document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop)
let height = window.innerHeight
let loadIndex = 0

if (top > height * 5) {
loadIndex = 4
} else if (top > height * 2.5) {
loadIndex = 3
} else if (top > height * 1) {
loadIndex = 2
} else if (top > this.height) {
loadIndex = 1
}

let min
this.floorLoaded.map((item, index) => {
if (item == false && !min) {
min = index
}
})
if(min && loadIndex>min) {
this.getLazyData(loadIndex)
}
})
}
}

为保证首屏数据加载速度,分屏加载的数据需要在首屏数据加载完成之后再执行,所以定义一个首屏数据加载完成的标识firstScreenLoad,只有当首屏数据加载完成后才去执行分屏数据加载。通过监听滚动事件,当滚动到第二屏时加载下一屏的数据,但是在第二屏里为了避免数据反复加载,我们需要对已经加载的数据做过滤处理,所以定义一个已加载楼层的数组floorLoaded,初始化全部为false,当每加载一屏时,就将数组相应的标识置为true,然后将加载的屏数和已加载楼层的序号做对比,只有当加载屏数大于已加载楼层的序号数时才去进行数据加载,如此就可以避免每次滚动都会触发数据加载的问题。

lazy-load

图片懒加载

实现方式:

  1. 设置图片src属性为同一张图片,同时自定义一个data-src属性来存储图片的真实地址
  2. 页面初始化显示的时候或者浏览器发生滚动的时候判断图片是否在视野中
  3. 当图片在视野中时,通过js自动改变该区域的图片的src属性为真实地址

基于以上功能我们引入vue-lazyload,在main.js进行引入插件并配置,。

1
2
3
4
5
6
import VueLazyLoad from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1.5,
attempt: 2,
loading: '//img30.360buyimg.com/uba/jfs/t22357/176/210555046/9106/323dc062/5b03bd29Nb8dda14d.jpg'
});

vue文件中将需要懒加载的图片绑定 v-bind:src 修改为 v-lazy

1
<img v-lazy="cate.pictureUrl" :key="cate.pictureUrl" alt />

但是vue使用swiper循环播放,如果同时用了懒加载VueLazyload,轮播一圈之后第一张显示空白,解决思路就是首尾两张图片不用懒加载,中间的所有图片都使用懒加载,代码:

1
2
<img v-if="index===0||index==(swiperData.list.length-1)" :src="cate.pictureUrl" alt />
<img v-else v-lazy="cate.pictureUrl" :key="cate.pictureUrl" alt />

无限下拉加载

对于有很多相似条目需要展示的页面,可以用无限下拉的方式来避免资源浪费,只有当用户操作向下滚动才加载更多的条目。
具体思路就是监听滚动事件,页面首先加载一屏数据,当滚动到一定位置再加载一批,以此类推。思路可能很简单:判断滚动到当前元素,然后动态添加dom内容。但是实现起来可能有很多小问题:如何判断是否滚动到当前元素;是在某个div里滚动还是整个页面滚动;每次加载时记录第几次加载;加载失败的处理等。
此处使用了自定义指令,定义了一个v-scroll指令。
首先定义几个参数

1
2
3
4
5
6
7
8
data() {
return {
energyList: null, // 数据总列表
energyRender: null, // 数据渲染列表
times: 1, // 数据已经加载次数
count: 0 // 数据需要加载总次数
};
},

然后给对应参数赋值,这里要对不足偶数的商品做一次隐藏处理,因为页面模块是一行展示两个,然后这里以一屏6个商品为例,所以首次渲染列表为总列表的前6个商品。
1
2
3
4
5
6
7
8
9
10
11
12
watch: {
energyData: function (newVal, oldVal) {
if (newVal) {
this.energyList = this.energyData.nengliangbujizhan.list;
if (this.energyList.length % 2 !== 0) {
this.energyList.pop();
}
this.energyRender = this.energyList.slice(0, 6);
this.count = Math.floor(this.energyList.length / 6);
}
}
},

接下来就是我们最关键的自定义指令,主要就是监听页面的滚动事件,判断页面是否滚动到当前元素,当滚动到当前元素则执行loadMore函数,再加载一屏数据,直至数据加载完成,实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
directives: {
scroll: {
bind: function (el, binding) {
window.addEventListener('scroll', () => {
const top = (document.documentElement.scrollTop ||
window.pageYOffset || document.body.scrollTop);
const height = window.screen.height;
if (top + height >= el.offsetHeight) {
const fnc = binding.value;
fnc();
}
});
}
}
},
methods: {
loadMore() {
if (this.times <= this.count) {
const newAry = [];
for (let i = 0; i < 6; i += 1) {
if (this.energyList.length > (this.times * 6) + i) {
newAry.push(this.energyList[(this.times * 6) + i]);
}
}
this.energyRender = [...this.energyRender, ...newAry];
this.times += 1;
}
}
}

最后在需要按需加载的模块的外层dom元素上增加v-scroll="loadMore"属性,内层数据列表循环取energyRender列表的数据即可。

总结

作为一名前端工程师,谈起性能优化已经是老生常谈了,但是不可否认的是,web前端性能优化依旧是前端非常重要的一个知识点,而网页的加载速度是一个重要指标,不论是预加载还是懒加载,两者都是提高页面性能有效的方法。本篇文章从加载的角度出发进行了总结,或许,并不全面,但是都是一些平时开发中会被经常用到的知识。帮助有需要的小伙伴更好的学习性能优化。