记一次web中对svg图形拖动和缩放卡顿的性能优化(基于svg.js) - viewbox vs transform

1 篇文章 1 订阅

前言

  • 在web中对svg图形的操作,我使用svg.js库挺久的了,对svg图形的拖动缩放我一直用的衍生插件svg.panzoom.js

  • svg.panzoom库对svg拖拽缩放是对viewBox的设置来实现的,至于使用viewBox来缩放、拖拽的原理,建议各位百度了解一下,不细说了(以后有空可以讲解一下)
    在这里插入图片描述

  • 最近使用这个库拖拽svg发现了一个比较严重的性能问题(其实之前一个项目就发现了,不过当时没有这么卡顿且需求是只用同时预览一张图即可),对性能没有太大要求,便没有多管,现在这个项目是多tab页预览svg图形,对性能有一定要求,见下图:
    在这里插入图片描述
    gif图中,可以发现,拖拽异常的卡顿,这才6张svg图(实际上打开一张也卡),有时候直接拖动不了了

  • 本文使用到的类和方法基本由svg.js库提供,如有不明白的地方,查看官方文档

猜想为什么会卡顿?

在以前做过渡动画时,曾经对height、width、margin等属性设置了transition,发现这样设置过渡动画,一旦元素多了后会异常的卡顿,这是因为改变这些属性时会触发浏览器重排大量耗费性能,而transform既不会触发重排也不会触发重绘,是交给gpu渲染,关于浏览器的重排、重绘、硬件加速的详细原理各位可以百度一下“transition 卡顿”、“浏览器重排、重绘、gpu加速”,可以看看这篇文章这篇,我在这里就不多说了(没有深入了解)

优化过程

方案1(引用其他库)

  • 直接使用现有开源库当然是最方便省事的啦,在github中找了几个star比较高的库:svg-pan-zoompanzoom

  • 使用panzoom库,发现几乎不存在卡顿,并且它的平滑滚动(smoothScroll)很不错,效果如下:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 使用svn-pan-zoom库,效果没有panzoom库好:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 这两个库都是对g元素的transform来实现缩放的,svg-pan-zoom必须传入svg元素并且会使用一个g元素包裹svg内的所有元素;而panzoom对传入元素没有限制,但只能对传入的指定元素进行移动缩放

  • 然而本方案有个致命缺点,这两个库都会自动删除svg的viewBox属性,意味着没有办法使用svg的viewBox坐标系了,而这两个库提供的pan和zoom方法都是传入基于Dom坐标系的clientX、clientY,这将影响到现有系统的业务功能(视图中移动到svg图中到指定的元素),所以此方案暂时抛弃

方案2(viewBox+requestAnimationFrame)

  • window.requestAnimationFrame可以让DOM在每一帧中集中处理DOM操作,试试这个神器能不能提高性能?

  • 分别在wheelZoom、panning事件中加上requestAnimationFrame,对svg.panzoom.js代码修改如下:

    在这里插入图片描述
    在这里插入图片描述

  • 先使用小一点的图测试实际效果:

    在这里插入图片描述

  • 使用大一点的图测试实际效果:

    在这里插入图片描述

  • 如果连续触发拖动开始、拖动结束,那么会异常的卡(黄点是鼠标按下):

    在这里插入图片描述

  • 拖动时不会卡顿了,但在浏览大图时整体流畅度依然不高(fps低),并且拖动开始和拖动结束的时候会卡顿一下,此时我还没有找到这个问题原因(见方案4)

方案3(g transform+requestAnimationFrame+拖动结束修改viewBox)

  • 参考方案1的原理,既然transform性能效果这么好,那么能不能既提高拖拽性能,又不删除svg的viewbox属性呢,想到如下方案:在panning时,仅对指定的g元素(可以把svg所有内容都放进去)transform实现拖动、缩放,在panEnd时将transform变化到svg的viewBox上,再配合window.requestAnimationFrame
// 注:this.panZoomAgentTransform类型是svg.js提供的Matrix类
// on panning
if (this.panZoomAgent) {
  // 有代理元素的时候不需要去计算viewbox
  this.panZoomAgentTransform.translateO(deltaX, deltaY)
  } else {
  this.viewbox = this.viewbox.transform(new Matrix().translate(-deltaX, -deltaY))
}

// on zoomming
if (this.panZoomAgent && this.isPanning ) {
  // 在拖动时,如果有代理元素的时候不需要去计算viewbox
  this.panZoomAgentTransform.scaleO(ratio, focus.x, focus.y)
  } else {
  // 非拖动时,直接计算viewbox
  this.viewbox = this.svg.viewbox().transform(new Matrix({ scale: 1 / ratio, origin: focus }))
}

// 在拖动结束时
if (!this.panZoomAgent || !this.panZoomAgentTransform) return
// 这里需要反转transform,因为坐标系是反的
this.viewbox = this.viewbox.transform(this.panZoomAgentTransform.inverse())
// 结束时,移除代理元素的transform属性
this.panZoomAgent.node.setAttribute('transform', '')
this.panZoomAgentTransform = null
this.svg.node.setAttribute('viewBox', this.viewbox.toString())
  • 为什么不保持g元素的transform,要在panEnd时对viewBox去transform呢?因为当时想到这个方案的时候,我并不想去变动svg的坐标系,因为有个功能需要定位到指定的元素,如果保持指定g元素的transform,那么svg中所有元素坐标都变了,而当时想了好久都没有想到怎么计算transform后的坐标系
  • 实际效果,可以发现在mousedown(panStart)和mouseup(panEnd)时,还是会卡一下:
    在这里插入图片描述
    带上开发者工具的gif
    在这里插入图片描述

方案4(g transform+requestAnimationFrame)

  • 参考方案1的原理,使用一个g元素(代理元素)将svg内所有内容包裹起来,所有的拖动缩放都只影响g元素的transform,不去在panEnd时去设置viewBox了,并且不删除viewBox,可以继续使用svg坐标系

  • 本方案与方案3大体一致,区别就是在panEnd时不再去设置svg的viewBox,因为在方案3中浏览大一点的svg图时方案3在panStart、panEnd时会卡顿,所以我以为是设置viewBox导致浏览器重排/重绘影响了性能(实际上并不是),接着往下看

  • 直接上大图测试下实际效果,可以发现在拖动过程中流畅度已经很不错了:

    在这里插入图片描述

  • 让我百思不得其解的是,这明明跟方案1是一样的,为何在连续触发拖动开始、拖动停止时如此的卡顿?

    在这里插入图片描述

  • 在开发者工具中,用性能工具监控一下看看panStart和panEnd到底发生了什么:

    在这里插入图片描述
    在这里插入图片描述

    把时间轴拉远一点,看看由于重新计算样式导致浪费了多少性能

    在这里插入图片描述

  • 可以发现,在panStart、panEnd耗费了进400毫秒去重新计算样式,通过查看调用的函数发现是为svg元素加上了一个类名,因为我对svg.panzoom库进行了修改,当拖动开始时会设置svg的class、userSelect、cursor,而设置元素的内联样式会导致浏览器重排,代码定位如下:
    在这里插入图片描述
    在这里插入图片描述

  • 原因找到了,注释这段代码试试:

    在这里插入图片描述

    重新计算样式只用了5毫秒

    在这里插入图片描述
    在这里插入图片描述

    拉远时间轴,并没有发现耗时长的任务了

    在这里插入图片描述

至此,卡顿问题解决了,如果要使用方案4的话,还要解决坐标系问题来实现zoom和panTo方法,
比如一个元素的x=1180,y=1157,那么如何根据当前svg缩放、位移偏差(就是代理g元素的transform),来将此元素移动到svg图中心位置呢?可以使用svg.js提供了的类和方法来实现:Point类的transofrm方法,具体代码如下:


  /**
   * 重新实现svg实例的zoom方法(当仅使用代理元素时)
   * @param {number} lvl
   * @param {import('@svgdotjs/svg.js').CoordinateXY} focus
   * @returns
   */
  zoomTo(lvl, focus) {
    if (lvl == null) return this.zoomLevel

    const viewbox = this.original.viewbox

    let zoomDelta = lvl / this.zoomLevel

    if (!focus) {
      focus = {
        x: viewbox.cx,
        y: viewbox.cy
      }
    }
    let realFocus = new Point(focus).transform(this.panZoomAgentTransform)

    this.panZoomAgentTransform.translateO(viewbox.cx - realFocus.x, viewbox.cy - realFocus.y).scaleO(zoomDelta, viewbox.cx, viewbox.cy)

    this.zoomLevel *= zoomDelta

    this.panZoomAgent.node.setAttribute('transform', this.panZoomAgentTransform.toString())
  }
  
   /**
   * 将指定坐标、元素移动到svg图的中心位置
   * @param {Point|Element|import('@svgdotjs/svg.js').CoordinateXY} point 必须是Svg中的元素或svg的viewbox坐标系
   * @param {number} zoomlvl 缩放等级
   * @param {number} duration 动画的持续时间
   * @returns
   */
  panTo(point, zoomlvl, duration = 500) {
    if (typeof this.panTo.runner === 'number') {
      window.cancelAnimationFrame(this.panTo.runner)
    } else if (this.panTo.runner instanceof Runner) {
      this.panTo.runner.finish()
    }
    this.panTo.runner = null

    try {
      const viewbox = this.original.viewbox

      /** @type{SvgElement} */
      let element
      if (point instanceof Element) {
        element = point

        // 拿到目标元素的盒子
        let elBox = element.bbox()

        // 将目标元素的中心店作为目标坐标
        point = {
          x: elBox.cx,
          y: elBox.cy
        }

        if (zoomlvl === 'auto') {
          // 长边占可视区域的10%
          zoomlvl = 0.1 / Math.max(elBox.height / viewbox.height, elBox.width / viewbox.width)
        }
      } else if (point === 'fit-center') {
        // 移动到svg图原始中心点
        point = {
          x: viewbox.cx,
          y: viewbox.cy
        }
      }

      let zoomDelta = zoomlvl / this.zoomLevel,
        // 将g元素的transform变化到坐标后,就是新的坐标了
        realPoint = new Point(point).transform(this.panZoomAgentTransform)

      // 想要移到svg图中心位置,使用原始的viwbox中心点减去目标坐标点,就是偏移量了
      this.panZoomAgentTransform.translateO(viewbox.cx - realPoint.x, viewbox.cy - realPoint.y).scaleO(zoomDelta, viewbox.cx, viewbox.cy)

      this.zoomLevel *= zoomDelta
      if (duration > 16) {
        this.panTo.runner = this.panZoomAgent.animate(duration).transform(this.panZoomAgentTransform)
      } else {
        // 无动画
        this.panZoomAgent.transform(this.panZoomAgentTransform)
      }

      return this
    } catch (error) {
      console.error('[SVG] panTo出错: ', error)
    }
  }

优化总结

  • 浏览器重排会导致svg的拖拽卡顿,避免在拖拽开始、过程中、结束时设置svg元素的class,优先使用transform
  • 想省事直接用开源库,性能:方案4≈方案1>方案3>方案2,坐标系计算:方案2=方案3>方案4>方案1
  • 在方案1中删除掉viewBox后,如何移动定位到svg中指定元素,目前还没有想到如何去计算坐标
  • 真是踩了不少坑,前前后后花费了将近3-4天时间,其实如果不是在panStart、panEnd时去设置了svg的class属性导致dom重新计算样式浪费性能,那么在实施方案2的时候已经基本满足性能需求了,可能就不会去尝试方案3、4了,
  • 然而不尝试方案3、4,也不会发现仅仅设置一下svg的class属性竟然会导致这么大的性能损耗,并且方案3、4我在思考如何计算transform后的svg坐标系也花费了不少时间:使用viewbox方案实现的panTo只需要传入实际坐标即可,而transform方案需要计算目标坐标变化后的实际位置,再跟原始盒子大小计算translate差值

使用性能工具监控对比

以三次panStart、panEnd来测试(方案2-4已注释设置svg的class属性代码以提升性能)

  • 方案1
    panzoom库
    在这里插入图片描述

    svg-pan-zoom库,此库没有使用requestAnimationFrame,果然性能较差

    在这里插入图片描述
    在这里插入图片描述

  • 方案2,每一次对viewBox的更新都需50ms左右

    在这里插入图片描述
    在这里插入图片描述

  • 方案3,可以发现在mouseup时,将transform设置到viewBox上还是有一点点的性能损耗,gpu利用率明显的提升

    在这里插入图片描述
    在这里插入图片描述

  • 方案4

    在这里插入图片描述

后语

  • 源码见此处
  • gif截图软件:ScreenToGif
  • 关键字:transform、web中硬件加速、requestAnimationFrame
  • 使用svg.js也快两年了,一直想编写一篇关于svg.js的学习教程,去年就已经创建好草稿了,奈何一直没时间动笔(下次一定),最近遇到了这个问题就赶紧记录下来了
    在这里插入图片描述
  • 前端小白一个,如有不足请指出
  • 25
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云帆Plan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值