杂谈:电商平台中的图片资源优化实战(代码片段)

恪愚 恪愚     2022-12-22     313

关键词:

图片渲染优化

以前谈过许多次图片问题。也给出了几种方案。在实际使用中这几种无疑是可行而且方便的:

  1. loading
  2. connection API + promise.all()异步加载图片
  3. 骨架屏
  4. 懒加载 + 占位图

但是在电商场景下,第一种方案是不可以的:我们不能为了一张图片而放弃整个内容对用户的正常展现。(尤其是这个图片还只是个背景图)

第二种方案在首页是有奇效的,但是笔者觉得限制太多 —— 如果对于“通用型”的方案来说,骨架屏似乎更适合中大型项目。
这个方案当时提出的场景是:在我校的实验系统首页会有一个超大的轮播图,但是它的作用只是展示和宣传。很显然,这个轮播图并不需要所有的图片都能展示出来,但是如果放任不管,在网络情况稍差的时候会有空白甚至闪白的问题出现。这是不好的!所以用connection API检测当前网络情况以决定展示轮播图还是一张图片。

最后一种方案应该是现在比较常用的。占位图还可以换做一个固定宽高的、有背景颜色的div,减少一次图片的请求(以前去哪儿首页就这样做的)。
但是它的缺陷在于:如果图片太大,长时间的loading也会让用户觉得烦躁!

不做处理是不可能的。长时间的白屏或者闪白估计会让产品发疯。。。


雪碧图

CSS spirte是个好东西,它能大幅减少 http 开销。spirte + prefetch的组合可以让优化 http 请求的同时让其提早被加载:

<link rel="prefetch" href="xxx" />

prefetch是对浏览器的“暗示”:将来可能需要某些资源,但由浏览器决定是否加载以及什么时候加载这些资源。它的优先级比较低。

spirte的原理是将整个整个页面需要的图片都放到一张图中,通过transform移动到需要展示的地方。但还是那句话:如果有许多体积太大的图片,会让雪碧图的加载异常困难,从而带来不好的用户体验。

懒加载

刚进入网页页面就会有大批量的图片资源加载,这会间接影响页面的加载,增加白屏加载时间,影响用户体验。因此,我们的诉求就是不在可视化窗口内的图片不真正加载,尽可能减少本地带宽的浪费和请求资源的数量。

懒加载的优势很明显:

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

实现简单的懒加载

实现的方式有两种:

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

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

笔者之前专门研究过 预加载和懒加载(点击查看文章),在其中封装了一个函数可供调用。但那是通过监听 scroll 的方式。
事实上,浏览器提供的IntersectionObserver API要更方便一些:

const root = 获取父元素;
const options = 
    root: root,
    // 这里是一个数组可以指定多个比例类似[0.25, 0.5, 0.75, 1]
    threshold: [0],//交会处
    rootMargin:"0px"//对视口进行收缩和扩张

const lazyIntersection = new IntersectionObserver(entires => 
    // entires为监听的节点数组对象
    entires.forEach((item,index) => 
        console.log(item)
        // console.log(item.target, item.isIntersecting? '可见': '不可见')
        // isIntersecting是当前监听元素交叉区域是否在可视区域指定的阈值内返回的是一个布尔值
        if(item.isIntersecting) 
            console.log('可见')
            item.target.src = item.target.getAttribute('data-src')
            // 这里资源加载后就停止进行观察
            lazyIntersection.unobserve(item.target)
        
        // console.log(item)
    )
, options)

let data = Array.from(document.querySelectorAll('img'))
data.forEach(item => 
    // observe用来观察指定的DOM节点
    lazyIntersection.observe(item)
)

大图片渲染优化

你也许见过类似这张图:

普通图片加载方式和后来被提出的jpg渐进式加载png交错式加载相比简直不值一提。
事实上,渐进式加载在实际使用中更加常见 —— 笔者推荐jpeg格式的渐进式加载方式:你不必完整的下载完毕图片,就可以看到图片的内容了。
没错它是一种由模糊到清晰的加载方式。

看到好多文章用JS模仿这种方式加载图片。但是…你把图片产生的 http 消耗转换为单线程js阻塞。结果是用户看到页面/可交互时间变长了。这样好么?

用 photoshop 生成图片时有个“存储为web所用格式”,然后,其中那个连续勾选就是渐进式JPEG图片了:
切记!需要勾选那个转换为sRGB选项,在某些浏览器下,图像设置为CMYK会出现一些问题!
FireWorks等图像软件也是有类似的输出设置的。

渐进式图片的优缺点

  1. 渐进式图片一开始大小框架就定好,不会像基本式图片一样,由于尺寸未设定而造成回流 —— 提高的渲染性能;
  2. 渐进式图片也有不足,就是吃CPU吃内存。

长图渲染优化

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

怎么办呢?
笔者研究了我司的图片处理工具:当你点开看大图时,会发现只显示了图片的一部分,或者在大图区域第一次只展示最顶部一小部分的内容。

依照这个思路,我们可以做相应的切图优化,将一张长图分成多个等比例大小的多张图块,来进行一个分批渲染调优,减少单次渲染长图的压力。

这个步骤显然是后端处理的 ——拿Node来说,你可以下载 gm1 或是 ImageMagic2

npm install --save gm

gm 中提供了用于剪裁的函数:

gm("img.png").crop(width, height, x, y)

ImageMagic 更加强大,不仅可以剪裁图片,还支持 图片格式转换 和 图片识别 等功能!

处理完成后可以存放在专门的目录下,然后监听 scrollIntersectionObserver API在用户下滑时不断请求“下一段”图片。

图片异常处理

图片的异常分为两种:加载异常渲染异常

加载异常

加载异常就是指“由于各种原因导致的图片请求失败”。因为图片属于资源,资源的异常会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而window.onerror不能监测捕获。
但可以被addEventListener捕获:

// 图片、script、css加载错误,都能被捕获
<script>
  window.addEventListener('error', (error) => 
  	console.log('捕获到异常:', error);
	, true)
</script>
<img src="https://yun.tuia.cn/image/kkk.png">
<script src="https://yun.tuia.cn/foundnull.js"></script>
<link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
  
// new Image错误,不能捕获
<script>
  window.addEventListener('error', (error) => 
    console.log('捕获到异常:', error);
  , true)
</script>
<script>
  new Image().src = 'https://yun.tuia.cn/image/lll.png'
</script>

// fetch错误,不能捕获
<script>
  window.addEventListener('error', (error) => 
    console.log('捕获到异常:', error);
  , true)
</script>
<script>
  fetch('https://tuia.cn/test')
</script>

渲染异常

渲染异常其实就是我们说的“页面上看不到图片、显示空白”。我们通常需要做的就是“降级处理” —— 比如给出一个失败的提示,或者给出降级的图片(比如用z-index加持的background、伪元素加持下的图片/SVG)。

切记!如果你用了loading做加载优化,不要让loading一直存在!


  1. gm官网:http://www.graphicsmagick.org/ ↩︎

  2. ImageMagic官网:https://imagemagick.org/ ↩︎

golang实战项目-b2c电商平台项目(代码片段)

Golang实战项目-B2C电商平台项目(6)商品新增中上传图片商品新增中上传图片使用的是KindEditor的多文件上传组件.文件上传为异步上传方式.可以上传多个文件.当文件上传后要求服务器端返回数据格式如下.应该是map转换为json而不应... 查看详情

golang实战项目-b2c电商平台(代码片段)

Golang实战项目-B2C电商平台(1)###--完成商品管理模块和CMS(内容管理模块)技术选型MySqlGolangEasyUIKindeditor使用MVC开发模式Model:模型层View:视图层Controller:控制器层项目搭建新建项目:ego,在Goland中修改GOROOT为当前项目路径(不修改无法build)... 查看详情

golang实战项目-b2c电商平台项目(代码片段)

Golang实战项目-B2C电商平台项目(3)总体页面的显示由于在main中声明的全局对象无法被其他包调用,所以在commons文件夹下新建CommonVars.go,保证整个项目任何包都可以使这个对象进行设置Handlerpackagecommonsimport"github.com/gorilla/mux"var(... 查看详情

golang实战项目-b2c电商平台项目(代码片段)

Golang实战项目-B2C电商平台项目(4)多表查询正确显示叶子类目查询完商品信息后可以通过商品信息中Cid做为商品类目表的主键值进行查询页面中最终数据的效果和TbItem只差一个属性所以可以通过组合的形式实现typeTbItemChildstruct TbIt... 查看详情

golang实战项目-b2c电商平台项目(代码片段)

Golang实战项目-B2C电商平台项目(5)实现商品上架本质是修改tb_item表中status=1之前在完成商品删除时已经在TbItemDao.go中编写了修改status值的函数,直接复用即可根据页面中内容,客户端给服务端发起请求后要求服务器端返回数据已... 查看详情

golang实战项目-b2c电商平台(代码片段)

Golang实战项目-B2C电商平台(2)实现登录功能用户登录后判断帐号和密码是否正确,其中帐号可以是用户名、电话、邮箱.服务器端判断后返回EgoResult结构体对应的JSON数据,其中Status=200表示成功,成功后页面跳转到/index,Status只要不为... 查看详情

go实战|电商平台图片上传到七牛云(代码片段)

文章目录1.获取配置信息2.config3.upload完整代码这一章节中我们介绍如何把图片上传到七牛云,并且返回对应图片的url。因为后续我们的创建的商品,更换头像等等都是把我们的图片上传七牛云存储,再返回对应的url的... 查看详情

golang实战项目-b2c电商平台项目(代码片段)

Golang实战项目-B2C电商平台项目(8)商品描述新增商品描述表(tb_item_desc)和商品表(tb_item)具有主外键关系,商品的主键也是商品描述的主键,使用工具函数生成的主键也当作商品描述表的主键商品描述中信息来源于页面中KindEditor的富文... 查看详情

golang实战项目-b2c电商平台项目(代码片段)

Golang实战项目-B2C电商平台项目(7)商品新增商品新增时需要新增的是两个表的数据tb_item和tb_item_desc表(tb_item_param规格参数暂时不考虑),在本小节中只新增tb_item表中数据,下一小节新增商品描述新增成功后服务端返回EgoResult对应的json... 查看详情

go实战|电商平台(10)搜索商品(代码片段)

文章目录1.搜索商品1.1路由接口注册1.2接口函数编写1.2.1service层1.2.2api层1.3服务函数编写1.4验证服务1.搜索商品1.1路由接口注册v1.POST("products",api.SearchProducts)1.2接口函数编写1.2.1service层定义一个搜索商品的服务结构体结构体... 查看详情

电商平台lnmp架构之mysql优化(代码片段)

电商平台lnmp架构之mysql优化1.Mysql的主从复制2.mysql一主两从3.基于GTID(全局事务标识)的主从复制4.半同步复制5.mysql异步复制6.延迟复制(SQL线程延迟)7.并行复制8.mysql的全同步复制(组复制)9.读写分离10.MHA高可用1.Mysql的主从... 查看详情

带你走向技术前沿,打造畅购电商平台实战(第一章)。(代码片段)

微服务系统架构概述单体应用架构以电商平台为例,如图所示,在传统单体应用中,所有模块代码,都会写在一个应用下,然后我们一般开发完成后,打成一个war包放到tomcat中去运行。随着用户流量的不断增长,业务复杂度的提... 查看详情

day814.电商系统表设计优化案例分析-java性能调优实战(代码片段)

电商系统表设计优化案例分析Hi,我是阿昌,今天学习记录的是关于电商系统表设计优化案例分析。如果在业务架构设计初期,表结构没有设计好,那么后期随着业务以及数据量的增多,系统就很容易出现瓶颈... 查看详情

go实战|电商平台展示商品(代码片段)

1.展示商品1.1路由接口注册GET请求获取商品列表v1.GET("products",api.ListProducts)1.2接口函数编写1.2.1service层创建一个展示商品的结构体typeListProductsServicestruct PageNum int `form:"pageNum"` PageSize int `form:"pageSize"... 查看详情

go实战|电商平台用户登录(代码片段)

文章目录1.路由注册2.接口编写2.1service层2.2api层2.3service3.登陆测试1.路由注册定义一个用户登录路由v1.POST("user/login",api.UserLogin)2.接口编写2.1service层定义用户登录的服务结构//UserLoginService管理用户登陆的服务typeUserLoginServicest... 查看详情

go实战|电商平台(12)删除商品(代码片段)

文章目录1.删除商品1.1路由接口注册1.2接口函数编写1.2.1service层1.2.2api层1.3服务函数编写1.4验证服务1.删除商品1.1路由接口注册authed.DELETE("product/:id",api.DeleteProduct)1.2接口函数编写1.2.1service层定义删除商品服务的结构体typeDelet... 查看详情

broadleaf电商平台上传图片出现nullpointerexception(代码片段)

Broadleaf-5.1.2+Tomcat7异常信息如下:严重:Servlet.service()forservlet[admin]incontextwithpath[/admin]threwexceptionjava.lang.NullPointerExceptionatorg.broadleafcommerce.common.audit.AbstractAuditableListener.s 查看详情

go实战|电商平台(11)更新商品(代码片段)

文章目录1.更新商品1.1路由接口注册1.2接口函数编写1.2.1service层1.2.2api层1.3服务函数编写1.4验证服务1.更新商品1.1路由接口注册authed.PUT("product/:id",api.UpdateProduct)1.2接口函数编写1.2.1service层创建更新商品的服务typeUpdateProductServ... 查看详情