故事背景
随着图满意项目的不断迭代,功能的不断叠加,时不时会有一些用户反馈卡顿,甚至页面崩溃的情况出现。
为了解决和定位问题,我们特意做了用户调研,以及尝试做一些性能监控捕获异常。在调研过程中,我们发现在用户配置较低的电脑上对于 2D 下的一些缩放事件会出现明显的卡顿现象,帧率大概下降到 20fps。
需求
在 2D 界面中,有一个背景网格一直在页面中显示。它的交互要求是:不论界面如何缩放,网格的线段粗细始终保持不变,否则将会出现非常不好看的粗线条以及细线条等。
除了 2D 网格需要此交互,项目中任意物体的描边也同样需要次交互需求,如:墙体描边,门窗描边,标注线等。
问题发现
在之前的实现过程中,是在缩放状态有改变的过程中,重新绘制所有需要使用到该线段宽度的Graphics
,也就是说,每一次鼠标的滚轮事件都将会触发到页面的 1px 线段重绘。
第一步需要做的优化工作当然是节流处理,让缩放值的状态每200ms
可被调用执行一次,这点就不在赘述。
之后在打开性能面板调试过程中,发现每次缩放事件的火焰图中,存在几个50ms
以上的动作,且 CPU 占用度较高。
如果要达到用户体验良好,最理想的 60FPS,那么需要每个 JS 调用事件时间小于 16ms。具体分析方式可以参考这篇文章:《让你的网页更丝滑》
很明显,这段方法中所调用的内容,就是最需要去优化的地方。而这段方法的含义就是缩放值变化,触发重新绘制网格或其他 1px 的线段的绘制。
看到绘制网格的方法中:
/**
* 绘制PIXI(2D)场景的网格
* @param {{}} opt
* @returns {Graphics}
* @param graphics
*/
export function drawGrid(graphics: Graphics, opt = {}) {
const options = {
size: 240,
lineWidth: 1,
step: 50,
lineColor: 0xd4d4d4,
lineColor2: 0xc0c0c0,
...opt
};
const unitGrid = graphics;
unitGrid.clear();
const unitNum = options.size;
const length = options.size * options.step;
unitGrid.width = length;
unitGrid.height = length;
// row column
const items = ["column", "row"];
for (const item of items) {
for (let i = 0; i <= unitNum; i++) {
const offset = i * options.step;
unitGrid.lineStyle(
options.lineWidth,
!(i % 10) ? options.lineColor2 : options.lineColor
);
if (item === "row") {
unitGrid.moveTo(0, offset);
unitGrid.lineTo(length, offset);
} else if (item === "column") {
unitGrid.moveTo(offset, 0);
unitGrid.lineTo(offset, length);
}
}
}
const sizePx = options.size * options.step;
unitGrid.position.set(-sizePx / 2, -sizePx / 2);
return unitGrid;
}
很明显,在这段方法中,来回循环绘制了240*240
条线段,每次缩放都做这样的绘制动作显然是非常耗费性能与时间的。
解决思路
那么有什么好的办法来解决这样的绘制动作呢?
试想一下,我的网格线段形状和位置应该是不需要改变的,需要改变的应该只是线段的某个属性才对。那么能否拿到最终的渲染数据,改变属性值来达到优化的目的呢?
显然 Pixi 的Graphics
的类本身是没有此类方法的。在查看了 Pixi 的源码后,终于发现,其实在Pixi.Graphics
中的绘制流程是这样的:
Graphics.prototype.drawShape = function drawShape(shape) {
if (this.currentPath) {
// check current path!
if (this.currentPath.shape.points.length <= 2) {
this.graphicsData.pop();
}
}
this.currentPath = null;
var data = new _GraphicsData2.default(
this.lineWidth,
this.lineColor,
this.lineAlpha,
this.fillColor,
this.fillAlpha,
this.filling,
this.nativeLines,
shape,
this.lineAlignment
);
this.graphicsData.push(data);
if (data.type === _const.SHAPES.POLY) {
data.shape.closed = data.shape.closed;
this.currentPath = data;
}
this.dirty++;
return data;
};
可以看到,每一个多边形、线段,都最终会被实例化为一个GraphicsData
的对象,最终 push 到this.graphicsData
中,然后在更新this.dirty
更新图形数据缓存,最终进入renderer
进行Buffer
转换,然后绘制渲染。
所以,完全可以扩展Graphics
来达到免重绘的,线段样式修改。
在项目起初,我们就自己封装了基于pixi.js
的库,用于补充、修改一些满足项目要求的扩展方法。此刻,只需要扩展Graphics
的原型链就可以达到我们想要的目的。
namespace PIXI {
import Graphics = PIXI.Graphics;
Object.assign(Graphics.prototype, {
updateLineStyle(options: {
lineWidth?: number;
color?: number;
alpha?: number;
alignment?: number;
}) {
if (!(this.graphicsData && this.graphicsData.length)) {
return null;
}
const graphicsData = this.graphicsData;
graphicsData.forEach(graph => {
if (options.hasOwnProperty("lineWidth")) {
graph.lineWidth = options.lineWidth;
}
if (options.hasOwnProperty("color")) {
graph.lineColor = options.color;
}
if (options.hasOwnProperty("alpha")) {
graph.lineAlpha = options.alpha;
}
if (options.hasOwnProperty("alignment")) {
graph.lineAlignment = options.alignment;
}
});
this.dirty++;
this.clearDirty++;
}
});
}
对比报告
之后,只需要在更新线段宽度时,调用graphics.updateLineStyle()
,即可快速有效的修改所有GraphicsData
的线段属性,已经可以满足项目需求。
最终测试,使用这种方法能够有效提高帧率,降低 CPU 占用率!
最终的优化效果对比:
绘制网格绘制耗时对比: