D3 实现水球图以及由其导致的性能问题的记录
水球图的实现
在线示例可以访问 Observehq Notebook
{/ right click element and select embed to copy the iframe url on Observable /}
<iframe style="margin-bottom: 1em;" width="100%" height="200" frameborder="0" src="https://observablehq.com/embed/70713e198a9a835b@327?cells=element%2CsetIntervalId" ></iframe>
圆环
使用 `d3.arc()` 即可以画出圆弧
渐变
`linearGradient` 在 svg 是一个 element,在 d3 中像对待普通的元素创建它就可以了
const defs = svg.append("defs");
const grad = defs
.append("linearGradient")
.attr("id", gradientId)
.attr("x1", points[0])
.attr("y1", points[1])
.attr("x2", points[2])
.attr("y2", points[3]);
grad
.append("stop")
.attr("offset", "0")
.attr("stop-color", config.get("gradientFromColor"));
grad
.append("stop")
.attr("offset", "1")
.attr("stop-color", config.get("gradientToColor"));
someShape.style("fill", "url(#" + gradientId + ")");值/单位,以及值的变化
已知开始值和结束值,使用 `d3.intepolate` 做一个插值,然后使用 `transition.tween` 自定义过渡动画中的行为。
需要注意的是,如果是自定义单位,「值+单位」整体需要水平居中对齐的话,由于 svg 中 <text> 中似乎是不能够嵌套 <text>,不如 html 中能够在 <span> 中嵌套 <span> 直接对最外层的 span 进行定位,导致这里的排版需要知道单位的文本宽度,可以使用 text.node().getComputedTextLength() 拿到,但注意这需要文本已经被渲染之后才可以拿到值。
水波以及波纹动画
定义一个 sin(x) 的 clipPath,水平方向做一个过渡动画,一个周期 x 方向的位移等于 sin(x) 周期 x 长度的整数倍,然后重新开始过渡,即可做出水波纹的效果。clipPath 对圆做一个切割,即得到半个水球。具体示例,详见 observehq notebook。
性能问题的解决
在项目的某个大屏展示页面上使用了该水球图(5个实例), 每隔 5s 水球图的数据会更新一次,之后发现,长时间切换到其他浏览器 tab 之后再回到这个页面,或者需要几秒钟才能恢复响应,或者直接崩溃。下面是一个 debug 的记录:
通过观察发现,当页面一直可见时,半小时、一小时之后,页面都没有卡,用浏览器的 performance 查看每一帧(16.6ms) 中水球图动画运行的时间(AnimationFrameFired)只有 0.14ms 左右,也都很均衡,。
当页面打开,并隐藏一段时间(30 分钟左右),再次返回到该 tab,发现页面暂时性的白屏,此时开始 performance 的录制,大概经过 4~5s 之后页面恢复响应,查看每一帧运行中动画脚本运行的时间
发现这个 AnimationFrameFired 的的事件非常奇怪:第一次的时候执行了足足有 1s 中,而这 1 秒钟内又有 40 次左右的 timerFlush (d3js 中用来延迟执行回调的函数)执行,而且其执行时间从 7ms 到 144 ms 之间,变化幅度大。
在这 1s 之后,又有几十次的 AnimationFrameFired , 而这一次每一个 AnimationFrameFired 只有一次 timerflush,且执行时间在 8.5ms 左右,是正常的 20 多倍
在这之后,时间已经来到了 4.5s , 后面的 AnimationFrameFired 的执行时长才恢复到 0.14ms 的正常水平
可见我们找到了页面卡顿的原因! 之前都是切换其他 tab 去干其他事情了,再回到这个 tab 发现页面卡死了,由于大量的 AnimationFrameFired 在页面唤醒时被执行,在其他页面停留的越久,就越多,导致了页面崩溃! 而短暂的停留,比如 30 分钟,计算机的性能还能够 handle 只是需要 4.5s 的反应时间, 而过了 12 小时,浏览器则崩溃了也不足为奇。
由此可以设想这是因为当页面隐藏时,浏览器的机制决定了分配给隐藏页面的资源会减少。联想到浏览器有一个 visibilitychange 的事件,查看 MDN 文档 的描述发现 :
Most browsers stop sending requestAnimationFrame() callbacks to background tabs or hidden
<iframe>s in order to improve performance and battery life.
大多数的浏览器都会停止 requestAnimationFrame 中回调的执行。页面进入了一个“休眠”的状态,有理由相信,这和我们的页面卡顿有着很强的关联性。于是,监听 visibilitychange 事件,当页面不可见的时候停止 水球图的更新,当页面恢复可见时,恢复动画的更新。
可以下载 Performance Profile 从 chrome devtools 中打开,即可自行分析。