百万PV商城实践系列 - 前端图片资源优化实战

⚠️ 本文为掘金社区首发签约文章,未获受权禁止转载css

前言

百万PV商城系列主要收录我在商城项目中的实践心得,我会从视觉体验性能优化架构设计等三个维度出发,一步一步为你们讲解商城项目中前端出现的问题问题解决的思路与方案,最后再进行代码实践。让你在工做中碰到相似问题时,可以更加驾轻就熟。前端

本篇是商城实践系列的第一篇文章,主要内容是对商城项目中常见场景的图片资源加载进行优化,提高视觉体验与页面性能。node

背景

在平常生活中,Yoyo们确定都用过一些电商公司的AppWeb小程序等应用。在浏览的过程当中,会有很是多琳琅满目的图片,好比常见的商品卡、商品详情、轮播图、 广告图等组件都须要使用一些后台管理配置上传的图片资源。react

除此以外,这些组件所在的页面也大都是流量承担很是大的页面,若是用户的体验感官比较差,必然会影响用户的留存转化。那么,本篇文章咱们就来学习一下图片资源优化的一些方案技巧,我会分别从普通图片、高保真图片、长屏渲染图这三个场景,依次给你们讲讲工做中的实战操做。若是碰到相应的优化场景,你就可以从容以对了。git

image.png

普通图片优化

对于普通图片优化,我以懒加载(lazyLoad)做为主要的实施手段。懒加载说白了就是优先加载可视窗口内的图片资源,而可视窗口外的内容只有滚动后进入可视窗口内才会进行加载。shell

那么,我为何会用懒加载,以及懒加载的实现方案究竟是什么呢?下面,我经过一个简单的React的图片懒加载方案的实现,来一步一步说明。小程序

为何使用懒加载?

通常来讲,刚进入网页页面就会有大批量的图片资源加载,这会间接影响页面的加载,增长白屏加载时间,影响用户体验。所以,咱们的诉求就是不在可视化窗口内的图片不尽兴加载,尽量减小本地带宽的浪费和请求资源的数量。后端

那么,为何我会推荐懒加载作为普通图片资源的主要实施手段呢?由于它有两大优势。浏览器

  • 减小带宽资源消耗,减小没必要要的资源加载消耗。
  • 防止并发加载图片资源致使的资源加载阻塞,从而减小白屏时间。

image.png

实现简单的懒加载

那么,懒加载怎么实现呢?实现的方式有两种。缓存

  • 经过scroll事件来监听视窗滚动区域实现。该方法兼容性好,绝大多数浏览器和WebView都兼容支持。
  • 经过IntersectionObserver API观察DOM是否出如今视窗内,该方法优势在于调用简单,只是部分移动端兼容没有上一种方式好。

两种形式都是在观察当前DOM是否出如今了可视窗口内,若是出现的话就将data-src中的图片地址赋值给src,而后开始加载当前的图片。

那么,下面咱们就开始着手实现一个基于scroll事件的懒加载示例吧。

页面布局

咱们先画一个基本的页面布局出来,主要是将视窗和图片加载出来。

const ImageLazy = () => {
  
  const [list, setList] = useState([
    1,2,3,4,5,6,7,8
  ])

  const ref = useRef<HTMLDivElement | null>(null)

  return (
    <div className="scroll-view" ref={ ref }> {list.map((id) => { return ( <div key={id} className="scroll-item"> <img style={{ width: '100%', height: '100%' }} data-src={ `${ prefix }split-${id}.jpg` } /> </div> ); })} </div>
  )
}
复制代码
.scroll-item {
  height: 200px;
}

.scroll-view {
  height: 400px;
  overflow: auto;
}
复制代码

能够看效果图,在页面上只显示了两张图片,但其实全部的图片都已经加载完了。

image.png

注册scroll事件

scroll-view绑定了ref以后,同时须要在useEffect中对scroll事件进行绑定和注销。

以下,我先获取当前组件全部的img元素(真实操做最好使用指定className),为ref.current进行addEventListener添加事件监听操做,而后在回调中执行对应的方法。

同时,在return的时候,也须要将其事件移除,避免形成一些意外状况。

useEffect(() => {
  const imgs = document.getElementsByTagName('img');
  console.log(ref.current, 'current')
  ref.current?.addEventListener('scroll', () => {
    console.log('listens run')
  })
  return (
    ref.current?.removeEventListener('scroll', () => {
      console.log('listens end')
    })
  )
}, [])
复制代码

以下图,在我滚动的时候,同时执行了ScrollCallback,控制台打印了不少执行结果,意味着咱们的事件已经添加成功了。

image.png

滚动回调 & 节流函数

经过下面的分析图,咱们能够看到clientHeight 是咱们视窗的高度,而在ScrollView当中每次滚动都会触发scroll方法回调,能够拿到当前页面视窗的滚动距离scrollTop

那么,咱们又如何判断元素是否出如今页面上呢?

经过元素的offsetTop属性,能够知道当前元素距离顶部的偏移距离。那么,当咱们拿到窗口的高度clientHeight,滚动的距离scrollTop,以及元素距离顶部的距离offsetTop,就能够推断出下面一套条件公式,经过视窗高度(dom.clientHeight) + 滚动距离(dom.scrollTop) > 元素距离顶部距离(image.offsetTop)来判断当前元素是否出如今页面可视范围内了。

image.png

将其转换为函数方法实现结果以下:

function scrollViewEvent (images: HTMLCollectionOf<HTMLImageElement>) {
    
  // 可视化区域高度
  const clientHeight = ref.current?.clientHeight || 0
  
  // 滚动的距离
  const scrollTop = ref.current?.scrollTop || 0
  
  // 遍历imgs元素
  for (let image of images) {
    if (!image.dataset.src) continue
  
    // 判断src是否已经加载
    if (image.src) continue
    
    //图片距离顶部距离
    let top = image.offsetTop
    
    // 公式
    if (clientHeight + scrollTop > top) {
     // 设置图片源地址,完成目标图片加载
      image.src = image.dataset.src || ''
      image.removeAttribute('data-src')
    }
  }
}
复制代码

在这里,我也经过ahook中的useThrottleFn作一点节流的小优化来避免频繁的进行函数回调。在500ms内,事件只会执行一次,避免额外执行带来的性能消耗。

import { useThrottleFn } from 'ahooks'

// 截流函数hook
const { run } = useThrottleFn(scrollViewEvent, {
  wait: 500
})
复制代码

同时,咱们将其放入到scroll事件回调中执行。不过,在组件一开始实际上是触发不了scroll事件,所以,须要咱们手动来初始化当前第一次页面中的图片数据。

useEffect(() => {
  const imgs = document.getElementsByTagName('img');
  console.log(ref.current, 'current')
  ref.current?.addEventListener('scroll', () => {
    run(imgs)
  })
  run(imgs)
  return () => {
    ref.current?.removeEventListener('scroll', () => {
      console.log('listens end')
    })
  }
}, [])
复制代码

到此,咱们的一个图片懒加载基本就实现完成了,咱们来看看效果吧。

image.png

对于懒加载来讲,每一个item最好设置一个高度,防止在一开始没有图片时,组件由于没有高度而致使页面元素暴露下视窗内致使懒加载失效。

高保真图片优化

对于高保真这类图片而言,不少都是由相关运营人员配置的活动图,通常在大促期间会有不少微页面,或者是图片连接,都是经过图片 + 热区的形式发布给用户浏览的。

所以,绝大多数运营诉求都是尽量清晰展现对应的图片。那么懒加载显然并不能很好的解决问题,所以我在原先懒加载的基础上,新增了一些加载状态给用户视觉上的体验感官,目前市面上产品主要使用骨架屏 或者是渐进式加载等方案来让图片显示过渡更加的平滑,避免加载失败或者加载图片卡顿的尴尬。

如何实现

经过下面的这张图,依旧先来梳理下实现逻辑。在图片组件中,分别有两张图进行轮流替换,当高清资源图加载完毕后,须要将骨架图或者缩略图隐藏,显示已经加载好的高清图。

image.png

图片组件

分析结束后, 能够跟我来实现一个简单的图片组件,经过img中的onLoad事件来判断须要显示的图片是否已经加载完了。经过对应的状态(status)来控制略缩图的显示和隐藏。下面我就以渐进式加载来做为案例,参考下图,咱们来实现一个简单的状态切换组件。

import React from "react";
import "./index.css";

interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
  thumb: string;
}

type ImageStatus = 'pending' | 'success' | 'error'

const Image: React.FC<ImageProps> = (props) => {
  const [status, setImageStatus] = React.useState<ImageStatus>('pending');

  /** * 修改图片状态 * @param status 修改状态 */
  const onChangeImageStatus = (status: ImageStatus) => {

    /** TODO setTime模拟请求时间 */
    setTimeout(() => setImageStatus(status), 2000)
  }

  return (
    <> <img className="image image__thumb" alt={props.alt} src={props.thumb} style={{ visibility: status === 'success' ? "hidden" : "visible" }} /> <img onLoad={() => onChangeImageStatus('success')} onError={() =>onChangeImageStatus('error')} className="image image__source" alt={props.alt} src={props.src} /> </>
  );
};

export default Image;
复制代码

经过filter: blur(25px);属性,对略缩图添加了一部分模糊效果,这样就能够避免一些马赛克图的尴尬,来达到部分毛玻璃,或者说是高斯模糊的一些小特效。

iShot2021-07-20 00.58.24.gif

经过onLoad加载完毕的事件,咱们作了一个简单的渐进式图片加载,那么相应的相似于骨架屏等其它的加载态也能够经过同样的状态判断来进行实现的。只是将缩略图换成了其它组件,仅此而已。

长渲染图优化

在商品详情页面,运营会配置一些商品的详细描述图文,不只对图片的质量会比较高,同时图片也会很是长。那么很显然,咱们并不可能说直接拿到图片就显示在页面上,若是用户的网速比较慢的状况下,页面上就会直接出现一个很长的白条,或者一张加载失败的错误图。这些很明显不是咱们想要的结果。

那么,该怎么办呢?

咱们先看下淘宝等电商平台的一个商品详情,当你点开看大图时,会发现只显示了图片的一部分,会分红不少张张大小一致的图片给咱们。

依照这个思路,咱们也作了相对应的切图优化,将一张长图分红多个等比例大小的多张图块,来进行一个分批渲染调优,减小单次渲染长图的压力。

image.png

切图成块

那么,下面我会从模拟后端长图切短图在将其切割好的图片依次显示在页面中进行展现。话很少说,咱们直接进入正题。

首先,结合下面的分析图,咱们的切图原理其实很是简单,将一张长图分红长宽相等的小图,若是最后一张不知足切割块高度的话直接将剩余高度给单切成图片。

image.png

那么,下面我就写一个简单的node代码来带你们实战一下切图的过程。

Node切图

以下图,我会模拟一张运营上传的一张长图,而后切割成若干份右边高200的短图(大小按照需求评估)。下面,咱们就来看下实现效果的教程和代码吧。

image.png

首先,安装sharp用于图片处理,image-size用于图片大小信息的获取。

# shell

yarn add sharp image-size
复制代码

引入对应的包后,经过image-size获取咱们须要切割的原长图的信息,好比widthheight

const sharp = require('sharp')

const sizeOf = require('image-size');

const currentImageInfo = sizeOf('./input.jpg');
复制代码

当拿到图片的高度和宽度的时候,那么意味着我能够经过一个while循环将切割的等份高度给计算好。

/** @name 每块大小 200px height */
const SPLIT_HEIGHT = 200

/** @name 长图高度 */
let clientHeight = currentImageInfo.height

/** @name 切割小图高度 */
const heights = []

while (clientHeight > 0) {
  /** @if 切图高度充足时 */
  if (clientHeight >= SPLIT_HEIGHT) {
    heights.push(SPLIT_HEIGHT)
    
    clientHeight -= SPLIT_HEIGHT
    
  } else {
    /** @else 切割高度不够时,直接切成一张,高度清0 */
    heights.push(clientHeight)
    
    clientHeight = 0
    
  }
}
复制代码

image.png

那么,当我知道了切割的图片大小后,就能够对切割好的heights遍历,经过裁剪偏移切割成为真实的图片,并生成新的文件并保存起来。

下面代码中,我建立一个marginTop偏移量,每切割一次,就会将其height累加向下偏移,直到切割图片到最后一页结束为止。此时mariginTop 为图片的高度。

/** @name 偏移量 */
let marginTop = 0

heights.forEach((h, index) => {
  sharp('./input.jpg')
    .extract({ left: 0, top: marginTop, width: currentImageInfo.width, height: h })
    .toFile(`./img/split_${index + 1}_block.jpg`).then(info => {
      console.log(`split_${index + 1}_block.jpg切割成功`)
    }).catch(err => {
      console.log(JSON.stringify(err), 'error')
    })
    marginTop += h
})
复制代码

以下图,img文件夹下多了一些零碎的图片,而后检查一下图片是否拼接完整,若是没有问题的话,那么咱们就完成了一个简单的长图切块的需求,下一步就是放到前端进行渲染了。

image.png

前端展现

前端拿到对应的切片后,直接拼凑在前端页面上展现,处理掉中间的缝隙或者和毛边后,和长图渲染毫无差异。我渲染时依旧是采用懒加载的形式作了简单的加载优化,总体效果以下:

image.png

看完了加载效果后,那么来看看加载的响应时间吧。

因为我切好的图片并无作文件上的优化,所以单张图存在体积过大,可是丝绝不影响首屏的加载,下面能够看一张在Flow3G下的加载时间对比。

对于单张长图意味着用户可能会看到4s左右的白屏或者是骨架屏,而切片后加载能够优先的将部份内容展示给用户。

image.png

解决毛边或者缝隙

本方案在最终实现时,可能会有部分瑕疵,具体切图方案和设备型号有关系。若是碰到问题,能够参考个人一些解决方案:

  • 第一种是经过vertical-center设置垂直中心值来解决基线对齐问题。
  • 第二种是将img设置成一个真实block元素解决。
  • 第三种是我经常使用的是经过flex-direction设置为column为子元素作垂直排列解决问题。
  • 第四种是经过background图片的方式解决问题。但这样作的话若是想使用懒加载,就须要更改部分css样式偏移来达到可视窗口显示。

参考资源

总结

本篇文章中,我讲了三种不一样图片的优化策略。市面上已经有不少开源库可以较为方便的实现懒加载渐进加载的方式。同时,对于骨架屏,不少组件库都有对应的组件,封装起来成本也较小。

所以,若是在项目中确实涉及不少图片资源,那么文章中提到的优化方案是我比较推荐的。

  • 能作成懒加载的尽可能不要全量加载
  • 给予用户必定的状态提示,骨架屏或者是过渡图能作尽可能别拉下。
  • 长图能切图尽可能切图,将其拆开来优化是很是方便的。
  • 能压缩的图片尽量去进行压缩。

对于一个商城项目来讲,它的挑战性不是在于功能的实现逻辑上,而在于部分视觉感觉与体验的优化上。若是以为文章对你有帮助,能够点个👍,给我加个油。若是对前端电商项目想了解更多的Yoyo们能够关注本专栏。

近期好文

尾注

本文首发于:掘金技术社区
类型:签约文章
做者:wangly19
收藏于专栏:# 百万PV商城实践系列 公众号: ItCodes 程序人生

相关文章
相关标签/搜索