IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 记一次web中对svg图形拖动和缩放的性能优化(基于svg.js) - viewbox vs transform -> 正文阅读

[JavaScript知识库]记一次web中对svg图形拖动和缩放的性能优化(基于svg.js) - viewbox vs transform

前言

  • 在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加速),关于浏览器的ui渲染线程逻辑、硬件加速的详细原理各位可以百度一下“transition 卡顿”,可以看看这篇文章,我在这里就不多说了

优化过程

方案1(引用其他库)

  • 直接使用现有开源库当然是最方便省事的啦,在github中找了几个start比较高的库: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,因为浏览大一点的svg图时方案3在panStart、panEnd时会卡顿,所以我认为是设置viewBox导致ui重绘影响了性能(实际上并不是),接着往下看

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

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

  • 在开发者工具中,用性能工具监控一下看看panStart和panEnd到底发生了什么:
    在这里插入图片描述
    在这里插入图片描述
    把时间轴拉远一点,看看由于重新计算样式导致浪费了多少性能 在这里插入图片描述

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

  • 原因找到了,注释这段代码试试:
    在这里插入图片描述
    重新计算样式只用了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)
    }
  }

优化总结

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

使用性能工具监控对比

以三次panStart、panEnd来测试(已注释设置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的学习教程,去年就已经创建好草稿了,奈何一直没时间动笔(下次一定),最近遇到了这个问题就赶紧记录下来了
    在这里插入图片描述
  • 前端小白一个,如有不足请指出
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-12 17:23:13  更:2022-03-12 17:25:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 6:38:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码