对于一般的渲染引擎来说,我们可以简单地拿到待渲染的数据,然后直接通过 Canvas/DOM/SVG 来将需要渲染的图形和内容渲染出来。

而在复杂场景下,比如需要自行排版的文本、表格和图形类,光是将要渲染的数据计算出来,便容易面临性能瓶颈,而对于样式多样、结构复杂的内容来说,绘制过程也同样会出现性能瓶颈。

本文我们主要针对 Canvas 绘制的场景,来考虑将绘制的流程分为收集和渲染两部分。

# 渲染数据的收集

很多时候,我们在后台数据库存储的只有图形的基本信息,对于需要排版计算的数据来说,则需要在拿到数据之后,再根据页面进行排版计算,完成后才能渲染到页面。

或许这样说会有些抽象,我们以表格的渲染为例来说明。

对于表格这样的产品来说,存储的往往是以单元格为基本单位的数据,如每个单元格的内容(可能是复杂的富文本、图片、图标结合)、样式(边框、背景色)、行列的宽高等。而在实际上页面渲染的时候,我们可能会根据行列宽高、每个单元格的边框线设置来绘制格子的布局。

除此之外,我们还可能需要考虑单元格内容是否会超出单元格,来判断是否需要截断渲染、是否需要换行显示等。这便要求我们需要对内容宽高进行测量,比如使用CanvasRenderingContext2D.measureText()测量文本宽度,这依赖了浏览器环境下的 API 能力。这意味着我们无法在后台提前计算好这些数据,因此无法通过提前计算来加速渲染过程。

于是,我们需要在前端拿到后台数据后,再进行相应的排版计算。

如果说一边计算一边绘制,则整个过程的耗时会比较长,用户也可能会看到绘制过程,该体验不是很友好。因此,我们可以在计算过程中,先把要最终绘制的数据结果先收集到一起,Canvas 绘制的时候则可以直接用。

# 收集与绘制的功能划分

我们可以根据绘制内容,划分为以下的收集器和渲染器:

  • 线段数据收集和绘制(如表头、边框线等)
  • 矩形数据收集和绘制(如背景色)
  • 图像数据收集和绘制(如图片)
  • 文本数据收集和绘制(如文字内容)
  • 其他数据收集和绘制

可见,收集器和渲染器的类型是一一对应的,渲染器在渲染的时候,可以直接从对应的收集器类型中获取数据,然后绘制。

还需要考虑一种情况,即相同的收集器和不同的收集器类型里,绘制内容有重叠时,需要考虑绘制堆叠的顺序。举个例子,单元格的文字需要在背景色上面,也就是说单元格的绘制需要比背景色要晚。

这意味着我们在收集的时候,还需要给收集的数据添加堆叠顺序,在绘制的时候,则按照堆叠顺序先后绘制。我们可以将收集器分为多个,每个收集器定义堆叠顺序,相同堆叠顺序的数据收集到一起。

其实,我们使用一个收集器,通过给数据添加渲染类型,来将不同类型的数据放在一起,方便统一管理。在渲染的时候,则先根据绘制类型和堆叠顺序进行排序,再进行绘制。

# 渲染数据享元

前面我们在《前端性能优化--Canvas 篇》一文中描述过,Canvas 上下文切换的成本消耗比较大,如果在复杂内容绘制的情况下,可能会导致性能问题。

使用收集器的一个好处是,我们可以将同类型同样式的渲染数据进行享元。对于样式完全一样的数据,收集器可通过对样式进行 hash 享元存储在一起。

这样绘制的时候,就可以将样式一样的内容一起绘制,减少 Canvas 上下文的切换。

举个简单的例子,假设线段的绘制支持颜色、粗细两种不同的样式,那么我们收集的时候可以将同颜色、同粗细的线段位置信息存储在一起:

class LineCollector {
  // 以线段样式为 key,存储线段的开始和结束位置,如 [[startX, startY], [endX, endY]]
  private renderInfos: { [key: string]: number[][] } = {};

  addRenderInfo(renderInfo) {
    const hash = this.generateKey(renderInfo);
    if (!this.renderInfos[hash]) {
      this.renderInfos[hash] = [];
    }
    this.renderInfos[hash].push([...renderInfo.position]);
  }

  private generateKey(lineProp: { color: string; thickness: numer }): string {
    return `${lineProp.thickness}-${lineProp.color}`;
  }
}

通过这样的方式,我们将要绘制的数据收集起来,方便 Canvas 进行更高效的渲染。

# 结束语

通过将 Canvas 渲染过程拆分为收集和渲染两部分,架构上更清晰的同时,在一定程度上提升了渲染的性能和效率。而这样的架构设计,也更方便后续做更多的拓展,我会在后续篇章继续介绍。

部分文章中使用了一些网站的截图,如果涉及侵权,请告诉我删一下谢谢~
温馨提示喵