Pixi 1px线段性能优化

Pixi 1px线段性能优化

故事背景

随着图满意项目的不断迭代,功能的不断叠加,时不时会有一些用户反馈卡顿,甚至页面崩溃的情况出现。

为了解决和定位问题,我们特意做了用户调研,以及尝试做一些性能监控捕获异常。在调研过程中,我们发现在用户配置较低的电脑上对于 2D 下的一些缩放事件会出现明显的卡顿现象,帧率大概下降到 20fps。

需求

在 2D 界面中,有一个背景网格一直在页面中显示。它的交互要求是:不论界面如何缩放,网格的线段粗细始终保持不变,否则将会出现非常不好看的粗线条以及细线条等。

除了 2D 网格需要此交互,项目中任意物体的描边也同样需要次交互需求,如:墙体描边,门窗描边,标注线等。

QQ20190328-203341-HD.gif

问题发现

在之前的实现过程中,是在缩放状态有改变的过程中,重新绘制所有需要使用到该线段宽度的Graphics,也就是说,每一次鼠标的滚轮事件都将会触发到页面的 1px 线段重绘。

第一步需要做的优化工作当然是节流处理,让缩放值的状态每200ms可被调用执行一次,这点就不在赘述。

之后在打开性能面板调试过程中,发现每次缩放事件的火焰图中,存在几个50ms以上的动作,且 CPU 占用度较高。

736a15ff-cfd1-fc7e-e2e3-c35e06747330.png

如果要达到用户体验良好,最理想的 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++) &#123;
      const offset = i * options.step;
      unitGrid.lineStyle(
        options.lineWidth,
        !(i % 10) ? options.lineColor2 : options.lineColor
      );
      if (item === "row") &#123;
        unitGrid.moveTo(0, offset);
        unitGrid.lineTo(length, offset);
      &#125; else if (item === "column") &#123;
        unitGrid.moveTo(offset, 0);
        unitGrid.lineTo(offset, length);
      &#125;
    &#125;
  &#125;
  const sizePx = options.size * options.step;
  unitGrid.position.set(-sizePx / 2, -sizePx / 2);

  return unitGrid;
&#125;

很明显,在这段方法中,来回循环绘制了240*240条线段,每次缩放都做这样的绘制动作显然是非常耗费性能与时间的。

解决思路

那么有什么好的办法来解决这样的绘制动作呢?

试想一下,我的网格线段形状和位置应该是不需要改变的,需要改变的应该只是线段的某个属性才对。那么能否拿到最终的渲染数据,改变属性值来达到优化的目的呢?

显然 Pixi 的Graphics的类本身是没有此类方法的。在查看了 Pixi 的源码后,终于发现,其实在Pixi.Graphics中的绘制流程是这样的:

Graphics.prototype.drawShape = function drawShape(shape) &#123;
  if (this.currentPath) &#123;
    // check current path!
    if (this.currentPath.shape.points.length <= 2) &#123;
      this.graphicsData.pop();
    &#125;
  &#125;

  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) &#123;
    data.shape.closed = data.shape.closed;
    this.currentPath = data;
  &#125;

  this.dirty++;

  return data;
&#125;;

可以看到,每一个多边形、线段,都最终会被实例化为一个GraphicsData的对象,最终 push 到this.graphicsData中,然后在更新this.dirty更新图形数据缓存,最终进入renderer进行Buffer转换,然后绘制渲染。

所以,完全可以扩展Graphics来达到免重绘的,线段样式修改。

在项目起初,我们就自己封装了基于pixi.js的库,用于补充、修改一些满足项目要求的扩展方法。此刻,只需要扩展Graphics的原型链就可以达到我们想要的目的。

namespace PIXI &#123;
  import Graphics = PIXI.Graphics;
  Object.assign(Graphics.prototype, &#123;
    updateLineStyle(options: &#123;
      lineWidth?: number;
      color?: number;
      alpha?: number;
      alignment?: number;
    &#125;) &#123;
      if (!(this.graphicsData && this.graphicsData.length)) &#123;
        return null;
      &#125;

      const graphicsData = this.graphicsData;
      graphicsData.forEach(graph => &#123;
        if (options.hasOwnProperty("lineWidth")) &#123;
          graph.lineWidth = options.lineWidth;
        &#125;

        if (options.hasOwnProperty("color")) &#123;
          graph.lineColor = options.color;
        &#125;

        if (options.hasOwnProperty("alpha")) &#123;
          graph.lineAlpha = options.alpha;
        &#125;

        if (options.hasOwnProperty("alignment")) &#123;
          graph.lineAlignment = options.alignment;
        &#125;
      &#125;);

      this.dirty++;
      this.clearDirty++;
    &#125;
  &#125;);
&#125;

对比报告

之后,只需要在更新线段宽度时,调用graphics.updateLineStyle(),即可快速有效的修改所有GraphicsData的线段属性,已经可以满足项目需求。

最终测试,使用这种方法能够有效提高帧率,降低 CPU 占用率!

最终的优化效果对比:

ad38c073-8435-ddef-5293-038f890cc5b5.png

绘制网格绘制耗时对比:

ac876016-4cb8-39db-9621-02ccde2aa083.png

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×