优化图片和视频的加载过程,提升用户体验

展示效果

(因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵)

展示都是基于 Slow 3G 弱网下的效果。

优化前

before.gif

这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。

优化后

eeeee.gif

图片下载过程中显示模糊的图片占位符,直到图片下载完成再切换展示。

原理

首先先贴出页面的代码 index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    html,
    body {
      margin: 0;
      padding: 0;
    }

    @keyframes pulse {
      0% {
        opacity: 0;
      }
      50% {
        opacity: 0.1;
      }
      100% {
        opacity: 0;
      }
    }

    .container {
      width: 50vw;
      background-repeat: no-repeat;
      background-size: cover;
    }


    .container.loaded::before {
      animation: none;
      content: none;
    }

    .container::before {
      content: '';
      position: absolute;
      inset: 0;
      opacity: 0;
      animation: pulse 2.5s infinite;
      background-color: var(--text-color);
    }

    .container img,
    .container video {
      opacity: 0;
      transition: opacity 250ms ease-in-out;
    }

    .container.loaded img,
    .container.loaded video {
      opacity: 1;
    }
  </style>
  <body>
    <!-- container容器加载一个体积非常小的低分辨率图片 -->
    <div class="container" style="background-image: url(http://localhost:3000/uploads/10007/fox-small.jpeg);">
      <!-- 图片延时加载 loading: lazy -->
      <img
        src="http://localhost:3000/uploads/10007/fox.jpeg"
        loading="lazy"
        style="width: 50vw"
      />
    </div>
    
    <br/>

    <video
      id="video"
      autoplay
      controls="controls"
      style="width: 50vw"
      poster="http://localhost:3000/uploads/10007/big_buck_bunny-small.png"
      src="http://localhost:3000/uploads/10007/big_buck_bunny.mp4"
    ></video>
  </body>
  <script>
    const blurredImageDiv = document.querySelector('.container');
    const img = blurredImageDiv.querySelector('img');
    function loaded() {
      // 图片下载完之后 再展示
      blurredImageDiv.classList.add('loaded');
    }

    if (img.complete) {
      loaded();
    } else {
      img.addEventListener('load', loaded);
    }
    
    var poster = new Image();
    poster.onload = function () {
      // 加载完之后替换 poster url 不会重复请求
      const video = document.querySelector('#video');
      video.poster = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
    };
    poster.src = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
  </script>
</html>

其实原理就是基于原图片生成出一个低分辨率体积非常小的图片(因为体积小,下载会很快),然后作为占位符显示,直到原图片完全下载之后再替换展示原图片。

那么如何生成一个超低分辨率的占位图片呢,可以使用 ffmpeg,需要本地提前安装,我是用的MacOS系统,所以直接通过 brew install ffmpeg 安装了。

如果是服务使用 Docker 部署的话,可参考:

FROM node:16 AS deps
WORKDIR /app
COPY . .
RUN wget https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.4.1-arm64-static.tar.xz &&\
    tar xvf ffmpeg-4.4.1-arm64-static.tar.xz &&\
    mv ffmpeg-4.4.1-arm64-static/ffmpeg /usr/bin/ &&\
    mv ffmpeg-4.4.1-arm64-static/ffprobe /usr/bin/ 
#RUN apt install -y ffmpeg
RUN yarn install
RUN yarn build
EXPOSE 3000
ENV PORT 3000
CMD [ "node", "dist/index.js" ]
ffmpeg -i sourcePath.jpg -vf scale=width:height outputPath.jpg
// 约束比例压缩
// width/height 为压缩之后图片的宽高 当其中一个值为 -1 的时候将保持原来的尺寸比例压缩

那么我们可以有如下命令:

ffmpeg -i sourcePath.jpg -vf scale=20:-1 outputPath.jpg
// 压缩之后生成 20 像素宽的图片用于做占位符展示

我们可以写个文件上传的服务,上传图片之后,服务端自动生成一个低分辨率的图片版本,然后将两者的地址url都返回过来。比如 Node 中我们可以使用 fluent-ffmpeg,那么以上命令就对应成代码:

import * as ffmpeg from 'fluent-ffmpeg';
import { FfmpegCommand } from 'fluent-ffmpeg';

export const runFfmpegCmd = (command: FfmpegCommand) =>
  new Promise<void>((resolve, reject) => {
    command
      .on('error', (error) => {
        reject(error);
      })
      .on('end', () => {
        resolve();
      })
      .run();
  });


public async uploadImage(user: User.UserInfo, file: Express.Multer.File) {
    console.log(file);
    const path = join(this.uploadPath, `${user.id}`, '/');

    await ensureDir(path);

    const { originalname, path: filePath } = file;
    const finalPath = path + originalname;
    const name = originalname.split('.');
    const smallPath = path + name[0] + '-small.' + name[1];
    console.log(smallPath);
    await rename(filePath, finalPath);

    // size 对应 scale=20:-1
    await runFfmpegCmd(ffmpeg(finalPath).size('20x?').output(smallPath));


    return {
      statusCode: HttpStatus.OK,
      data: {
        path: finalPath,
        smallPath,
      },
    };
  }
  
public async uploadVideo(user: User.UserInfo, file: Express.Multer.File) {
    console.log(file);
    const path = join(this.uploadPath, `${user.id}`, '/');

    await ensureDir(path);

    const { originalname, path: filePath } = file;
    const finalPath = path + originalname;
    const name = originalname.split('.');
    const shotName = name[0] + '.png';
    const smallName = name[0] + '-small.png';

    await rename(filePath, finalPath);

    // 生成两个不同分辨率的缩略图
    await Promise.all([
      runScreenShotCmd(
        ffmpeg(finalPath).screenshot({
          count: 1,
          filename: shotName,
          folder: path,
        }),
      ),
      runScreenShotCmd(
        ffmpeg(finalPath).screenshot({
          count: 1,
          filename: smallName,
          folder: path,
          size: '20x?',
        }),
      ),
    ]);

    return {
      statusCode: HttpStatus.OK,
      data: {
        path: finalPath,
        shotPath: path + shotName,
        smallPath: path + smallName,
      },
    };
  }

代码在自己的github上:im_server

自己本地的 swagger 界面的上传截图:

图片
image.png

视频
image.png

那么我们就可以得到一个超低分辨率的图片了,由于体积非常小,所以下载很快(特别是弱网情况下)。

补充

关于 img 标签的 lazy load 可参考:浏览器IMG图片原生懒加载loading=”lazy”实践指南

使用 imgsrcset 属性可实现根据不同屏幕分辨率加载不同尺寸的图片,进一步提升用户体验,而且没必要在小屏幕中加载超大分辨率的图片:响应式图片

结论

通过使用超低分辨率的占位符图片可以优化用户体验,特别是一些图片素材网站,再结合 img 标签的 loading="lazy" 懒加载。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY9P30vM' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片