长列表图片优化方式

最近在的一个项目中,本来不长的列表页变得很长很长。并且每个li都会有一个图片。这就迫使我必须要想办法优化这个长列表了。以前总以为是不是对于长列表而言性能的瓶颈更多在与 dom 和js, 但是实际上图片仍然也占很大一部分。为了用户考虑,怎么样减少图片带来的流量,对于节省带宽及用户的电池十分重要。

长列表可以从以下这几个方面去优化:


图片懒加载

图片懒加载,长列表最大的问题就是图片太多,如果一次性把图片全部请求了,那么页面渲染速度会很慢,如果用户点不到,还会造成很大的浪费,甚至会有性能瓶颈。

为什么要使用懒加载呢?为了加速页面的加载速度,减少不必要的请求,我们可以将未出现在可视区域的图片先不做加载,等到滚动到可视区域以后再去加载。这样提升了性能和提高了用户体验。

那么原理就很简单了:一开始时,所有图片都有一个默认的 src, 指向本地的一个 default.png ,并且把img的真实的地址放在 data-src上。当滚动时,判断元素是否在可视区域,如果在可视区域,那么再把 data-src 上的值写入真正的 src 中。


从图片本身优化

第二种思路就是从图片的本身去优化,比如更换图片格式。由 png,jpg,jpeg,gif 转换为更好的 webp。

WebP 的优势在与它更好的图像数据压缩算法,能够将图片转换为更小的体积,具有无损和有损的压缩模式。如果是选择了有损压缩,也拥有肉眼无法识别差异的图像质量。虽然它在页面渲染的时候浏览器比jpg会花稍长的时间解析它的算法,但是权衡它所带来的体积的减少来看,WebP 还是最优秀的。

关于它的有损和无损来说,无损的体积减少会稍微小一些,而有损的体积会减少的非常多。如果不是对图片质量要求很多,对于一般的图片,用有损就很好了。webp无损压缩可以减少图片一半的大小而达到同样无损的效果。可以看下面一份数据:

YouTube的视频缩略图采用WebP后,网页加载速度提升了10%;谷歌网上应用商店采用WebP后,每天可节省几TB的带宽,页面平均加载时间大约减少1/3;谷歌移动应用市场采用WebP图片格式后,每天节省了50TB的存储空间;2014年腾讯新闻客户端应用了WebP后,流量峰值带宽降低9GB,网络连接延时不变的前提下,平均图片延时和数据下载延时降低了100ms;2014年空间装扮也全量转换成WebP,带宽上也有显著降低。

但是 WebP 不是每个浏览器都支持。所以要实践的话可以从下面两个方面考虑:

从服务端考虑:
如果浏览器支持 WebP ,那么会在 request header accept里,发送image/webp, 服务器收到以后根据这个来去返回给客户端图片。请求头里有,则发送webp的,如果没有,就发送普通jpg的。

不过这个对服务器要求比较高,并且现在图片大多放在 CDN 上,CDN去做这种策略可能会稍微麻烦点。

从前端考虑:
前端去检测浏览器是否支持 WebP, 支持就发送 WebP 的图片请求,不支持就发送 JPG,PNG 等的。下面是一行代码判断是否支持WebP。

1
var isSupportWebp = !!\[\].map && document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') == 0;

!!\[\].map主要是判断是否是IE9+,以免toDataURL方法会挂掉。如果你直接对数组原型扩展了map方法,则需要使用!!\[\].map以外的方法进行判断,例如!!window.addEventListener等。

哈哈,这个我是偶然在zhangxinxu的博客上看到的,感觉这个要比其他的方法更加简洁和好。地址在这里。

最后我没有做这一层优化,因为图片太多,已经被发到了CDN 上,图片源可能都不在了,没有让他们去把图片变成 WebP。 如果一开始就问 CDC 的同事要两种图片格式就好了。sad。


代码实现1

首先有两种判断元素是否在可视区域的方法:

  1. offsetTop - scrollTop < clientHeight ,代表在可视区。
  2. IntersectionObserver,这个 Api 可以直接来判断。

注意可以用 throttle 防止请求次数过高,在一定的时间范围内只请求1次。下面的代码是随意写的,没有跑过。可以当做伪码来看。

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
const checkVisible = (ele) => {
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let clientHeight = document.documentElement.clientHeight

return ele.offsetTop - scrollTop < clientHeight
}

let imgs = document.getElementsByTagName("img"), nums = imgs.length

const lazyLoad = () => {
for (let i = 0; i < nums; i++) {
if (checkVisible(imgs[i]) && imgs[i].getAttribute("src") === "default.png") {
imgs[i].src = imgs[i].getAttribute("data-src")
}
}
}

const throttle = (action, delay) => {
let timeout = null
let lastRun = 0
return function () {
if (timeout) {
return
}
let elapsed = Date.now() - lastRun
let context = this
let args = arguments
let runCallback = function () {
lastRun = Date.now()
timeout = false
action.apply(context, args)
}
if (elapsed >= delay) {
runCallback()
} else {
timeout = setTimeout(runCallback, delay)
}
}
}

document.addEventListener("scroll", throttle(lazyLoad, 1000))


代码实现2

因为项目本身是Vue写的,所以我也去在网上找了相应的directives, 已经有了很好的一些实现。他们的比较完整,比如下面这个:https://github.com/hilongjw/vue-lazyload,用法很简单:

1
2
3
4
5
Vue.use(VueLazyload, {
preLoad: 1.3,
attempt: 2,
listenEvents: ["scroll"]
})

然后再 img 标签上,使用这个 directivev-lazy 即可:

1
<img class="mod-service__pic" v-lazy="list.icon" aria-hidden="true" v-bind:alt="list.name">


总结

在做这个过程中,我还看了一些其他的内容,比如 png8, png24, svg,这些。也去看了下vue-lazyload这个库的源码。收货多多。