前端体验优化之图片加载闪烁

最近在学习一些前端优化用户体验的方案,想到了在业务中碰到的因为图片加载导致页面撑开闪烁的问题,经过一番研究之后找到了一些可行的方案,于是记录一下。

<!--more-->

参考:

1. 背景

在某些设计场景中(如图文混排),我们往往需要图片按比例缩放展示,这时候我们只会限制图片的宽度或高度。

由于网络图片加载需要时间,就会导致图片在加载完成前后,页面出现“闪烁”的情况:原本没有空白的区域被图片撑开,页面高度变化,页面排版变化,从视觉体验上来看,并不是很友好。

加载前

加载完成后

一种解决办法是使用骨架屏,骨架屏可以避免在数据加载返回前页面为空的状态,但对于数据返回后网络图片加载闪烁,并没有解决。

2. 提前获取图片尺寸

图片闪烁的原因是:们在图片加载完成前,并不知道图片的原始尺寸。如果我们在渲染页面的时候知道了图片的尺寸,就可以通过一个占位盒预留图片需要的大小,避免内容突然被加载后的图片撑开了。

2.1. 预加载

拿到一个图片的url之后,我们可以通过预加载的方式拿到图片的原始尺寸

function getImageSize(src) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.src = src;
    image.onload = function () {
      const { width, height } = image;
      resolve({ width, height });
    };

    image.onerror = reject;
  });
}

大部分CDN厂商都提供了获取图片基本信息的接口,比如图片基本信息(imageInfo) - 七牛云,其本质上和图片预加载没有区别,都是通过网络请求提前拿到图片信息,这里不再展开。

预加载原本的目的是提前加载某些耗时的资源,让用户可以在真正使用这些资源的时候更顺畅。

但预加载也存在的一些问题

  • 预加载会浪费流量资源,加载完成之后用户可能并没有真正看到这些资源
  • 需要异步等待知道图片尺寸拿到之后,才开始渲染页面,用户会看到一段时间的加载或白屏界面,涉及到业务代码的改动

既然如此,有没有直接同步获取图片尺寸的方式呢?

2.2. 从链接上获取

如果在图片上传时,我们将图片的尺寸信息直接放在文件名上,那么就可以在拿到图片url时直接解析参数,获取图片的尺寸。

第一步:在图片上传前,可以从file文件读到文件的尺寸

function getImageSize(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = function(blob) {
      const image = new Image()
      image.src = blob.target.result
      image.onload = function() {
        const { width, height } = this
        resolve({ width, height })
      }
    }
  })
}

然后在上传时将图片尺寸拼接在文件名上面

async function uploadImage(file){
  const { width, height } = await getImageSize(file)
  const path = `/upload/${width}x${height}/${file.name}`
  await upload(file, path)
}

文件上传之后,就可以通过https://xxx.cdn.com/upload/100x200/xxx.png这种链接来访问图片了。

第二步:从链接上直接解析得到图片的原始尺寸

当拿到图片url时,可以按照上传时拼接的规则提取图片尺寸参数

function parseSize(url){
 const re = /\/(\d+)x(\d+)\//;
  const [_, width, height] = re.exec(imgSrc) || [];
  return { width, height };
}
parseSize('https://xxx.cdn.com/upload/100x200/xxx.png') // {width: 100, height:200}

bingo!这样在同步解析的情况下,我们就拿到了图片的原始尺寸了。

这个方案对于业务测的改动比较小,但要求前端业务方对于文件上传这些功能有定制的能力,此外对于历史上传的没有尺寸的图片,需要做兼容处理。

3. 根据图片尺寸提前占位

知道了图片的实际尺寸(宽高比),也知道了图片需要展示的样式宽或高,我们都可以轻松计算出另外一个尺寸的大小,根据这个大小,我们就可以生成一个占位的盒子来保证图片真实渲染后不撑开页面内容了

下面的计算都以知道图片样式的宽度,计算图片需要的高度为例。

3.1. padding占位

根据包含块的定义,某些属性被赋予百分比值时,是根据其包含块来计算的。其中widthpaddingmargin是由包含块的width来计算的。

根绝这个规则,我们就可以通过padding-bottom生成一个图片的占位盒子

.image-container {
  position: relative;
  height: 0;
  overflow: hidden;
  padding-bottom: 50%; // 宽高比2:1的图片

  .image {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}

对应的HTML结构

<div  class="image-container" :style="{paddingBottom}">
   <img :src="src" class="image" alt="">
</div>

其中的paddingBottom就是通过计算得到的图片高比宽

const paddingBottom = `${height / width * 100}%`

3.2. aspect-ratio

aspect-ratio属性可以为box容器规定一个期待的纵横比。其默认值类似于

img, input[type="image"], video, embed, iframe, marquee, object, table {
  aspect-ratio: attr(width) / attr(height);
}

根据这个属性,我们只需要在img标签上设置好图片的widhtheight属性,浏览器就会自动预留一个占位区域!

<img :src="src" width="200" height="100" alt="">    

还需要设置一下img的高度为auto

img {
    max-width: 100%;
  height: auto;
}

通过Chrome 的 network throttling调试一下,可以发现在图片还没加载时,浏览器就已经预留了的相关尺寸的区域,这样即使图片加载完成,也不会因为突然撑开内容导致内容闪烁了。

唯一的缺点是这个属性存在兼容性问题,iOS safari要在15之后才支持,在不考虑兼容性的情况下,aspect-ratio是改动成本最小最优雅的方案。

4. 完整代码

基于上面的思路,可以封装一个公共的图片组件,这里以Vue3举例

  • 对于链接上不可以解析出尺寸的历史图片,直接作为img标签渲染
  • 否则,计算出图片的比例,生成占位容器,考虑到目前的兼容性问题,仍然使用包含块pandding来处理,后续可以考虑修改为aspect-ratio
<template>
  <div v-if="aspectRatio" class="image-container" :style="{paddingBottom:aspectRatio}">
    <img :src="props.src" class="image" alt="">
  </div>
  <img :src="props.src" v-else class="image" alt="">
</template>

<script lang="ts" setup>
import { computed } from 'vue';

type Props = {
  src: string
}
const props = defineProps<Props>();

// 解析链接中的尺寸参数,用于设置图片比例,避免图片加载导致页面闪烁
const contentSize = computed(() => {
  const re = /\/(\d+)x(\d+)\//;
  const [_, width, height] = re.exec(props.src) || [];
  // return { width: undefined, height: undefined };
  return { width, height };
});
const aspectRatio = computed<string>(() => {
  const { width, height } = contentSize.value;
  if (!width || !height) return '';
  return `${parseFloat(height) / parseFloat(width) * 100}%`;
});
</script>

<style scoped lang="scss">

.image-container {
  position: relative;
  height: 0;
  overflow: hidden;

  .image {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}
</style>

图片图片还可以提供一些其他体验优化的功能,比如占位图、懒加载、图片点击放大等功能,这里不再展开,可以参考vant-image的实现。

5. 小结

要避免图片加载闪烁,我们就需要同步获取到图片尺寸,这样才能在图片加载前进行占位。

要同步获取图片尺寸,就需要在上传图片时,将图片尺寸信息放在url中,这样就可以通过解析url获取图片尺寸。

通过这两步,看起来就可以解决图片加载带来的页面闪烁问题,对于优化内容展示页面还是挺有帮助的。