海报编辑
海报设计的编辑器一开始采用的是 DOM 设计方案,后续考虑到性能问题,以及后续规划复杂图形和特效问题, 改为采用Pixi.js作为核心渲染器的方式。
其基于 WebGL 渲染,能充分利用 GPU 加速,在处理大量复杂图形和动画时,性能优势明显。例如,当海报包含众多动态元素,像大量的特效、复杂的图形变换时,PixiJS 可以保持流畅的渲染,帧率稳定,用户操作也不会出现明显的卡顿。
然而当越来越多用户使用生成海报时,编辑器的性能问题就越来越明显,尤其是每逢重要营销节假日时,不少地区用户反馈平台卡顿等问题。为此特意排查,从用户操作类到资源加载都存在不少问题,其中不仅仅有 PixiJS 相关问题,也有 vue 的问题,本次篇幅将对 PixiJS 相关问题进行排查,并给出解决思路。虽然上面提到用 PixiJS 的好处以及后续扩展的便利,但在一些 DOM 容易实现的领域,PixiJS显得很麻烦难处理。
文本编辑卡顿
用户在海报上编辑文本是再常规不过的操作了,然而在 PixiJS 中,文本的编辑和渲染是远远不如 DOM 方式便利的,文本编辑和文本渲染显示分开处理,需要自行实现光标定位、文本选中,textArea 事件监听处理等等。用户在编辑文本的时候,会通过 TextEditor 组件将输入的信息更新到自定义的 Text 组件中,从而实现编辑和渲染分离。
这次的优化点发生则是发生在 TextEditor 组件更新文本信息到 Text 组件,会触发自定义的 updateStyle 方法,只是该 updateStyle 方法并没有太多复杂操作,代码形如下面:
export class Text extends PIXIText implements StuffObject {
// 省略其他代码
public updateStyle = () => {
const textInfo = Text.handleComponentData(this.componentData);
const text = textInfo.text;
this.text = text;
this.style = style;
this.x = position.x + this.width * this.anchor.x;
this.y = position.y + this.height * this.anchor.y;
if (textInfo.isUnderline) {
this.drawUnderline({ textInfo });
}
// ...省略其他代码
}
}
在 updateStyle 方法中,会先通过 Text.handleComponentData 方法处理文本信息,然后更新到 Text 组件中,更新文本 style 以及 position 等信息,似乎并没有很多太复杂的东西,甚至因为这里代码简单,调试性能问题的时候一度把这里忽略了,后续通过浏览器 performance 监控发现,这里存在性能问题,在编辑海量海报时,触发 updateStyle 方法,导致轻微的卡顿,苹果 M1 上大概 0.4ms,但是到了低端的window笔记本上,单次耗时可以达到 2ms 以上,那是哪里导致的问题呢?
上面代码其实暗藏很多操作,比如 this.text = text;this.style = style;,在 PixiJs 代码里面:
set text(text: string | number) {
text = String(text === null || text === undefined ? '' : text);
if (this._text === text) {
return;
}
this._text = text;
this.dirty = true;
}
set style(style: TextStyle | Partial<ITextStyle>) {
style = style || {};
if (style instanceof TextStyle) {
this._style = style;
}
else {
this._style = new TextStyle(style);
}
this.localStyleID = -1;
this.dirty = true;
}
每次修改 text 和 style 背后都会是一些系列的 set 动作,而这次导致性能问题,发生在 this.width 身上,没错不是 set 导致的,是 get width 导致的。
get width(): number {
this.updateText(true);
return Math.abs(this.scale.x) * this._texture.orig.width;
}
上面获取 this.width 的时候更新了文字的渲染,而文字渲染里面涉及比较重的操作 TextMetrics.measureText 甚至还有 scale 操作都会引起性能问题。可以发现一处简单的使用,居然能引起这么多性能陷阱。那要如何避免呢?这里使用 this.width 是因为文本存在中心旋转的操作,而中心旋转需要则,设置 this.anchor.set(0.5),需要更新 x、y 坐标的偏移量为 width 以及 hight 的一半。一个解决思路是缓存当前的 with,然后随着编辑过程,width 可能随便变化,于是采用另外一种方法 对于普通的编辑,非旋转的,则不需要设置锚点,直接设置偏移量即可,避免获取 this.width 导致页面性能下降,从而跳过性能修改问题。
然而上面的编辑改动只是涉及到一部分,真正的大头的是 TextEditor 组件优化,TextEditor 里面文本编辑,光标移动,选中复制等等操作,而光标里面则需要通过 TextMetrics.measureText 反复计算文本相关属性,导致性能降低,但是又不好通过缓存的方式,因为一直是在编辑中,一种比较好的思路是采用 textarea 元素代替 TextEditor 组件,这样通过原生的编辑方式,可以丝滑的避免光标以及其他复杂的文本操作。这个则是作为后续优化来处理。
图片裁剪
图片裁剪是海量海报中一个常见的需求,在涉及里面将已有的图片裁剪成指定的尺寸,为避免和原图片组件耦合,新建立一个裁剪组件,用户可以通过拖拉的形式,修改图片大小,操作确定后,在行成一个新的图片组件,然后替换掉之前的裁剪组件。如此设计是没有问题的,关键的性能问题发生在图片裁剪的的时候,原本方案如下:
const url = await app.renderer.extract.base64(this.croppedImage);
updateComponent(url)
通过这种方式简单的将裁剪后的图片 base64 赋值给图片组件,然后更新组件信息,但是这里引发了三个性能问题,一个是作为可配置组件,图片的地址是支持修改配置的,但是 base64 字符串长度过长,较大的图片基本会有 1M+ 的长度,进而导致 vue 属性传递到 el-input 组件时 rendering、painting 时间超长达到 700ms,第二个问题则是 extract.base64 操作是有一定耗时的,并且如果用户想要改回原图像,也无法支持;第三个则是编辑器数据保存的时候,较长的 base64 字符串会导致用户的 schema 数据过大,而且上传服务器耗时增加。
对于裁剪而言,并不需要生成完全新的图像,完全可以通过构建了一个新的 Rectangle 对象,此对象界定了图像纹理中想要显示的部分内容,主要实现如下:
// image.ts
// frame 设置
// x和y轴偏移 计算
// editCropperImage.ts
updateComponent(this.id, {
type: 'Image',
settings: {
textureOffset: x和y轴偏移,
textureScale: width和height的大小
position: 坐标,
size: 裁剪尺寸
}
});
裁剪后的图像,容器本身大小发生了变化,但是外部视觉看到的图像是没有缩放变化的,但是在 PixiJs 里面,需要对图像进行放大处理,同时还需要计算图像纹理的偏移,并且结合历史操作,最后生成纹理的偏移量和缩放比例。通过这些操作,能够丝滑的将裁剪后的图像完美的显示出来,避免重复上传桶资源,base64 生成等问题,而且有利于后续的裁剪改回原图像。
图像导出清晰度
多次接到地区同事反馈,平台编辑的海报看着是很清晰,下载下来就有点模糊,经过反复测试确实如此,文字部分是清晰的,但是图像纹理存在明显锯齿,尤其是部分像素密集的区域,比如二维码这些感觉细节少了。一开始设想方案是导出图片的时通过增大 resolution 也就是分辨率,来增加纹理细节:
// 创建渲染纹理,用于渲染页面到特定的尺寸和分辨率
const renderTexture = RenderTexture.create({
width: pageConfigStore.width * scale.x,
height: pageConfigStore.height * scale.y,
resolution: resolution ?? normalResolution,
});
app.renderer.render(page, { renderTexture });
// 从渲染纹理中提取Base64编码的图片URL
url = await app.renderer.extract.base64(renderTexture, format, quality);
通过渲染纹理,提高分辨率的方式,这里提高到两倍的方式,能大大增加系列,但是这个方案存在问题,比如导出的图片尺寸和编辑器尺寸不一致,尤其是设计场景,图像尺寸不一样,同时导出的图片体积也会大很多。其他的 AI 也有解答比如设置 this.texture.baseTexture.scaleMode = SCALE_MODES.LINEAR 不过实际都没有太大作用。那还有什么办法?
其实通过两倍分辨率可以获得足够多的纹理细节,canvas 缩放的时候存在 imageSmoothingEnabled 属性,这个属性可以对缩放后的图片进行平滑处理,而原本的图像问题主要是存在明显锯齿,过渡不协调的问题,那从两倍图再缩放到一倍图,使用 imageSmoothingEnabled = true 不就能平滑过渡图像?还能保证图像的尺寸一致性,岂不是两全其美。测试后虽然的确解决问题了,但是引入了新的问题,文字变的模糊了,imageSmoothingEnabled 的使用,不仅让图片纹理能平滑过渡,也让原本足够清晰的文字平滑过渡了,导致文字边缘出现明显的模糊问题。
文字渲染采用的是自定义的 Text 组件,该组件没有特殊的文本渲染操作,在平台显示的时候文字也是模糊的,但是导出图的时候,文字确实清晰的,就和图片导出变得锯齿一样,猜测是 PixiJs 导图机制限定的,其通过 gl.readPixels,也就是从 GPU 获取到对应的像素数据,然后再将获取到的像素数据转化为 ImageData 并绘制到 canvas 里面,从而获取到对应的导出图像。
gl.readPixels(
Math.round(frame.x * resolution),
Math.round(frame.y * resolution),
width,
height,
gl.RGBA,
gl.UNSIGNED_BYTE,
pixels
);
中间没有其他复杂操作,想要文字清晰可见的话,能不能通过修改文字的 baseTexture.scaleMode 呢?默认的方式是 SCALE_MODES.LINEAR,平台上看到的效果也是模糊的,如果想要下载的时候高清,能否修改 baseTexture.scaleMode = SCALE_MODES.NEAREST 呢?或则一开始就是这个方式,保证文字平台渲染也是清晰的?可惜的是,如此设置实时运行的文字是在 100% 的时候清晰的,但是一旦有缩放,则文字会出现残缺的情况,一开始以为就这样了,但是导出图像的时候,因为设置了 imageSmoothingEnabled = true 平滑过渡图像,导致缩放回一倍图的文字,从之前的残缺变得清晰。于是需要处理的就是下载时,**需要将所有文本的 baseTexture.scaleMode = SCALE_MODES.NEAREST,下载后再重置回去,通过反复测试性能,在比较复杂的文本海报里面,基本耗时在 3ms 属于可以接受的范围。**后续也会开放两倍图的下载,这样能够提供更多的纹理细节
Jank 监控
对于卡顿检测主要是通过 requestAnimationFrame 来进行检测,如果超过阈值则进行上报,不过这里需要主要的是,需要对连续的卡顿进行检测,如果最近 10 次检测间隔帧数低于阈值,则认为是低帧率卡顿,也需要上报。
海报编辑
海报设计的编辑器一开始采用的是 DOM 设计方案,后续考虑到性能问题,以及后续规划复杂图形和特效问题, 改为采用Pixi.js作为核心渲染器的方式。
其基于 WebGL 渲染,能充分利用 GPU 加速,在处理大量复杂图形和动画时,性能优势明显。例如,当海报包含众多动态元素,像大量的特效、复杂的图形变换时,PixiJS 可以保持流畅的渲染,帧率稳定,用户操作也不会出现明显的卡顿。
然而当越来越多用户使用生成海报时,编辑器的性能问题就越来越明显,尤其是每逢重要营销节假日时,不少地区用户反馈平台卡顿等问题。为此特意排查,从用户操作类到资源加载都存在不少问题,其中不仅仅有 PixiJS 相关问题,也有 vue 的问题,本次篇幅将对 PixiJS 相关问题进行排查,并给出解决思路。虽然上面提到用 PixiJS 的好处以及后续扩展的便利,但在一些 DOM 容易实现的领域,PixiJS显得很麻烦难处理。
文本编辑卡顿
用户在海报上编辑文本是再常规不过的操作了,然而在 PixiJS 中,文本的编辑和渲染是远远不如 DOM 方式便利的,文本编辑和文本渲染显示分开处理,需要自行实现光标定位、文本选中,textArea 事件监听处理等等。用户在编辑文本的时候,会通过 TextEditor 组件将输入的信息更新到自定义的 Text 组件中,从而实现编辑和渲染分离。
这次的优化点发生则是发生在 TextEditor 组件更新文本信息到 Text 组件,会触发自定义的
updateStyle方法,只是该updateStyle方法并没有太多复杂操作,代码形如下面:在
updateStyle方法中,会先通过Text.handleComponentData方法处理文本信息,然后更新到 Text 组件中,更新文本 style 以及 position 等信息,似乎并没有很多太复杂的东西,甚至因为这里代码简单,调试性能问题的时候一度把这里忽略了,后续通过浏览器 performance 监控发现,这里存在性能问题,在编辑海量海报时,触发updateStyle方法,导致轻微的卡顿,苹果 M1 上大概 0.4ms,但是到了低端的window笔记本上,单次耗时可以达到 2ms 以上,那是哪里导致的问题呢?上面代码其实暗藏很多操作,比如
this.text = text;this.style = style;,在 PixiJs 代码里面:每次修改
text和style背后都会是一些系列的set动作,而这次导致性能问题,发生在this.width身上,没错不是set导致的,是get width导致的。上面获取
this.width的时候更新了文字的渲染,而文字渲染里面涉及比较重的操作TextMetrics.measureText甚至还有scale操作都会引起性能问题。可以发现一处简单的使用,居然能引起这么多性能陷阱。那要如何避免呢?这里使用this.width是因为文本存在中心旋转的操作,而中心旋转需要则,设置this.anchor.set(0.5),需要更新 x、y 坐标的偏移量为 width 以及 hight 的一半。一个解决思路是缓存当前的 with,然后随着编辑过程,width 可能随便变化,于是采用另外一种方法 对于普通的编辑,非旋转的,则不需要设置锚点,直接设置偏移量即可,避免获取 this.width 导致页面性能下降,从而跳过性能修改问题。然而上面的编辑改动只是涉及到一部分,真正的大头的是 TextEditor 组件优化,TextEditor 里面文本编辑,光标移动,选中复制等等操作,而光标里面则需要通过
TextMetrics.measureText反复计算文本相关属性,导致性能降低,但是又不好通过缓存的方式,因为一直是在编辑中,一种比较好的思路是采用 textarea 元素代替 TextEditor 组件,这样通过原生的编辑方式,可以丝滑的避免光标以及其他复杂的文本操作。这个则是作为后续优化来处理。图片裁剪
图片裁剪是海量海报中一个常见的需求,在涉及里面将已有的图片裁剪成指定的尺寸,为避免和原图片组件耦合,新建立一个裁剪组件,用户可以通过拖拉的形式,修改图片大小,操作确定后,在行成一个新的图片组件,然后替换掉之前的裁剪组件。如此设计是没有问题的,关键的性能问题发生在图片裁剪的的时候,原本方案如下:
通过这种方式简单的将裁剪后的图片 base64 赋值给图片组件,然后更新组件信息,但是这里引发了三个性能问题,一个是作为可配置组件,图片的地址是支持修改配置的,但是 base64 字符串长度过长,较大的图片基本会有 1M+ 的长度,进而导致 vue 属性传递到 el-input 组件时 rendering、painting 时间超长达到 700ms,第二个问题则是
extract.base64操作是有一定耗时的,并且如果用户想要改回原图像,也无法支持;第三个则是编辑器数据保存的时候,较长的 base64 字符串会导致用户的 schema 数据过大,而且上传服务器耗时增加。对于裁剪而言,并不需要生成完全新的图像,完全可以通过构建了一个新的 Rectangle 对象,此对象界定了图像纹理中想要显示的部分内容,主要实现如下:
裁剪后的图像,容器本身大小发生了变化,但是外部视觉看到的图像是没有缩放变化的,但是在 PixiJs 里面,需要对图像进行放大处理,同时还需要计算图像纹理的偏移,并且结合历史操作,最后生成纹理的偏移量和缩放比例。通过这些操作,能够丝滑的将裁剪后的图像完美的显示出来,避免重复上传桶资源,base64 生成等问题,而且有利于后续的裁剪改回原图像。
图像导出清晰度
多次接到地区同事反馈,平台编辑的海报看着是很清晰,下载下来就有点模糊,经过反复测试确实如此,文字部分是清晰的,但是图像纹理存在明显锯齿,尤其是部分像素密集的区域,比如二维码这些感觉细节少了。一开始设想方案是导出图片的时通过增大 resolution 也就是分辨率,来增加纹理细节:
通过渲染纹理,提高分辨率的方式,这里提高到两倍的方式,能大大增加系列,但是这个方案存在问题,比如导出的图片尺寸和编辑器尺寸不一致,尤其是设计场景,图像尺寸不一样,同时导出的图片体积也会大很多。其他的 AI 也有解答比如设置
this.texture.baseTexture.scaleMode = SCALE_MODES.LINEAR不过实际都没有太大作用。那还有什么办法?其实通过两倍分辨率可以获得足够多的纹理细节,canvas 缩放的时候存在 imageSmoothingEnabled 属性,这个属性可以对缩放后的图片进行平滑处理,而原本的图像问题主要是存在明显锯齿,过渡不协调的问题,那从两倍图再缩放到一倍图,使用
imageSmoothingEnabled = true不就能平滑过渡图像?还能保证图像的尺寸一致性,岂不是两全其美。测试后虽然的确解决问题了,但是引入了新的问题,文字变的模糊了,imageSmoothingEnabled的使用,不仅让图片纹理能平滑过渡,也让原本足够清晰的文字平滑过渡了,导致文字边缘出现明显的模糊问题。文字渲染采用的是自定义的 Text 组件,该组件没有特殊的文本渲染操作,在平台显示的时候文字也是模糊的,但是导出图的时候,文字确实清晰的,就和图片导出变得锯齿一样,猜测是 PixiJs 导图机制限定的,其通过
gl.readPixels,也就是从 GPU 获取到对应的像素数据,然后再将获取到的像素数据转化为ImageData并绘制到canvas里面,从而获取到对应的导出图像。中间没有其他复杂操作,想要文字清晰可见的话,能不能通过修改文字的
baseTexture.scaleMode呢?默认的方式是SCALE_MODES.LINEAR,平台上看到的效果也是模糊的,如果想要下载的时候高清,能否修改baseTexture.scaleMode = SCALE_MODES.NEAREST呢?或则一开始就是这个方式,保证文字平台渲染也是清晰的?可惜的是,如此设置实时运行的文字是在 100% 的时候清晰的,但是一旦有缩放,则文字会出现残缺的情况,一开始以为就这样了,但是导出图像的时候,因为设置了imageSmoothingEnabled = true平滑过渡图像,导致缩放回一倍图的文字,从之前的残缺变得清晰。于是需要处理的就是下载时,**需要将所有文本的baseTexture.scaleMode = SCALE_MODES.NEAREST,下载后再重置回去,通过反复测试性能,在比较复杂的文本海报里面,基本耗时在 3ms 属于可以接受的范围。**后续也会开放两倍图的下载,这样能够提供更多的纹理细节Jank 监控
对于卡顿检测主要是通过
requestAnimationFrame来进行检测,如果超过阈值则进行上报,不过这里需要主要的是,需要对连续的卡顿进行检测,如果最近 10 次检测间隔帧数低于阈值,则认为是低帧率卡顿,也需要上报。