前言
-
在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-zoom、panzoom -
使用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
if (this.panZoomAgent) {
this.panZoomAgentTransform.translateO(deltaX, deltaY)
} else {
this.viewbox = this.viewbox.transform(new Matrix().translate(-deltaX, -deltaY))
}
if (this.panZoomAgent && this.isPanning ) {
this.panZoomAgentTransform.scaleO(ratio, focus.x, focus.y)
} else {
this.viewbox = this.svg.viewbox().transform(new Matrix({ scale: 1 / ratio, origin: focus }))
}
if (!this.panZoomAgent || !this.panZoomAgentTransform) return
this.viewbox = this.viewbox.transform(this.panZoomAgentTransform.inverse())
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)
至此,卡顿问题解决了,如果要使用方案4的话,还要解决坐标系问题来实现zoom和panTo方法, 比如一个元素的x=1180,y=1157,那么如何根据当前svg缩放、位移偏差(就是代理g元素的transform),来将此元素移动到svg图中心位置呢?可以使用svg.js提供了的类和方法来实现:Point类的transofrm方法,具体代码如下:
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())
}
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
let element
if (point instanceof Element) {
element = point
let elBox = element.bbox()
point = {
x: elBox.cx,
y: elBox.cy
}
if (zoomlvl === 'auto') {
zoomlvl = 0.1 / Math.max(elBox.height / viewbox.height, elBox.width / viewbox.width)
}
} else if (point === 'fit-center') {
point = {
x: viewbox.cx,
y: viewbox.cy
}
}
let zoomDelta = zoomlvl / this.zoomLevel,
realPoint = new Point(point).transform(this.panZoomAgentTransform)
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属性代码)
后语
- 源码见此处
- gif截图软件:ScreenToGif
- 关键字:transform、web中硬件加速、requestAnimationFrame
- 使用svg.js也快两年了,一直想编写一篇关于svg.js的学习教程,去年就已经创建好草稿了,奈何一直没时间动笔(下次一定),最近遇到了这个问题就赶紧记录下来了
- 前端小白一个,如有不足请指出
|