注册
web

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

展示效果


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


展示都是基于 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"

作者:梦想很大很大
来源:juejin.cn/post/7244352006814679100
code> 懒加载。

0 个评论

要回复文章请先登录注册