前言
最近看boss直聘上,WebAssembly 的需求挺多的,主要涉及音视频处理、图形图像、2D、3D、游戏开发等项目。
而最近我正好遇到一个二维图形优化的需求,所以就想看看WebAssembly 能否提高项目的运行速度。
1-WebAssembly 简介
WebAssembly简称wasm,是一个低级编程语言。
WebAssembly是便携式的抽象语法树,被设计来提供比JavaScript更快速的编译及执行。
WebAssembly可以让C/C++程序在浏览器内运行。
WebAssembly具有以下限制:
- 通常,WebAssembly 不允许与DOM直接交互。所有交互都必须通过 JavaScript 互操作进行。
- 没有垃圾收集(garbage collection)机制。
- 安全问题,比如容易隐藏恶意代码。
2-WebAssembly+canvas 2d 绘图测试
接下来我会通过Google的 canvaskit 工具来辅助测试WebAssembly+canvas 2d 的运行速度。
CanvasKit是以WASM为编译目标的Web平台图形绘制接口,其目标是将Skia的图形API导出到Web平台。所以我们可以直接用WebAssembly 开发web端的图形项目。
1.在自己的项目中安装canvaskit-wasm
npm install canvaskit-wasm
2.建立一个测试文件。
- 01-wasm绘图测试.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>test</title>
<style>
#foo {
border: 1px solid #ddd;
}
</style>
</head>
<body>
<canvas id="foo" width="700" height="600"></canvas>
<script src="/node_modules/canvaskit-wasm/bin/canvaskit.js"></script>
<script>
const { width, height } = document.getElementById('foo')
CanvasKitInit({
locateFile: (file) => '/node_modules/canvaskit-wasm/bin/' + file,
}).then((CanvasKit) => {
class Rect {
constructor(x = 0, y = 0, w = 100, h = 60) {
this.x = x
this.y = y
this.w = w
this.h = h
this.speed = 3
this.dirX = 1
this.dirY = 1
}
ani(width = 700, height = 600) {
const { x, y, w, h, speed } = this
if (x < 0 || x + w > width) {
this.dirX *= -1
}
if (y < 0 || y + h > height) {
this.dirY *= -1
}
this.x += this.dirX * speed
this.y += this.dirY * speed
}
draw(canvas, paint) {
const { x, y, w, h } = this
const rr = CanvasKit.XYWHRect(this.x, this.y, w, h)
canvas.drawRect(rr, paint)
}
}
const rects = []
for (let i = 0; i < 50000; i++) {
const rect = new Rect(Math.random() * 500, Math.random() * 500)
rect.speed = Math.random() * 20 + 1
rects[i] = rect
}
const surface = CanvasKit.MakeCanvasSurface('foo')
const paint = new CanvasKit.Paint()
paint.setColor(CanvasKit.Color4f(0, 0, 0, 0.01))
paint.setStyle(CanvasKit.PaintStyle.Stroke)
// paint.setAntiAlias(true)
let time = new Date()
function drawFrame(canvas) {
const newTime = new Date()
console.log(newTime - time)
time = newTime
canvas.clear(CanvasKit.WHITE)
rects.forEach((rect) => {
rect.ani(width, height)
rect.draw(canvas, paint)
})
surface.requestAnimationFrame(drawFrame)
}
surface.requestAnimationFrame(drawFrame)
})
</script>
</body>
</html>
在上面的代码中,我画了50000个透明的矩形,并且通过请求动画帧来驱动动画,效果如下:
console.log(newTime - time) 打印结果如下:
初次绘图用时206,之后的刷新频率在170-190之间,平均值可以取180。
接下来我们对比原生的canvas2d 看看。
2-原生canvas2d 绘图测试
接下来我依旧会画50000个透明的矩形。
- 02-canvas2d绘图测试.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>canvas2d绘图测试</title>
<style>
#foo {
border: 1px solid #ddd;
}
</style>
</head>
<body>
<canvas id="foo" width="700" height="600"></canvas>
<script>
class Rect {
constructor(x = 0, y = 0, w = 100, h = 60) {
this.x = x
this.y = y
this.w = w
this.h = h
this.speed = 3
this.dirX = 1
this.dirY = 1
}
ani(width = 700, height = 600) {
const { x, y, w, h, speed } = this
if (x < 0 || x + w > width) {
this.dirX *= -1
}
if (y < 0 || y + h > height) {
this.dirY *= -1
}
// move
this.x += this.dirX * speed
this.y += this.dirY * speed
}
draw(ctx) {
const { x, y, w, h } = this
ctx.strokeStyle = 'rgba(0,0,0,0.01)'
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x + w, y)
ctx.lineTo(x + w, y + h)
ctx.lineTo(x, y + h)
ctx.closePath()
ctx.stroke()
}
}
const rects = []
const rect = new Rect()
for (let i = 0; i < 50000; i++) {
const rect = new Rect(Math.random() * 500, Math.random() * 500)
rect.speed = Math.random() * 20 + 1
rects[i] = rect
}
const canvas = document.getElementById('foo')
const { width, height } = canvas
const ctx = canvas.getContext('2d')
let time = new Date()
!(function drawFrame() {
const newTime = new Date()
console.log(newTime - time)
time = newTime
ctx.clearRect(0, 0, width, height)
rects.forEach((rect) => {
rect.ani(width, height)
rect.draw(ctx)
})
requestAnimationFrame(drawFrame)
})()
</script>
</body>
</html>
效果和之前一样:
console.log(newTime - time) 打印结果如下:
初次绘图用时92,之后的刷新频率在80-90之间,平均值可以取85。
当前原生canvas2d 竟然比wasm快了一倍,我们暂且不做分析,再看一下WebGL的绘图速度。
3-原生WebGL 绘图测试
接下来我依旧会画50000个透明的矩形。
- 03-WebGL绘图测试.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>test</title>
<style>
#foo {
border: 1px solid #ddd;
}
</style>
</head>
<body>
<canvas id="foo" width="700" height="600"></canvas>
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec2 a_Position;
void main(){
gl_Position=vec4(a_Position,0,1.);
gl_PointSize=20.0;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
void main(){
gl_FragColor=vec4(0,0,0,0.01);
}
</script>
<script type="module">
class Rect {
constructor(gl, x = 0, y = 0, w = 0.2, h = 0.2) {
this.gl = gl
this.x = x
this.y = y
this.w = w
this.h = h
this.speed = 3
this.dirX = 1
this.dirY = 1
}
update() {
const { x, y, w, h, speed, gl } = this
const vertexBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([x, y, x + w, y, x + w, y + w, x, y + w]),
gl.STATIC_DRAW
)
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(a_Position)
}
ani() {
const { x, y, w, h, speed } = this
if (x < -1 || x + w > 1) {
this.dirX *= -1
}
if (y < -1 || y + h > 1) {
this.dirY *= -1
}
this.x += this.dirX * speed
this.y += this.dirY * speed
}
draw() {
this.update()
this.gl.drawArrays(gl.LINE_LOOP, 0, 4)
}
}
const canvas = document.querySelector('#foo')
const vsSource = document.querySelector('#vertexShader').innerText
const fsSource = document.querySelector('#fragmentShader').innerText
const gl = canvas.getContext('webgl')
gl.enable(gl.BLEND)
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE)
gl.clearColor(1, 1, 1, 1)
initShaders(gl, vsSource, fsSource)
const rects = []
for (let i = 0; i < 50000; i++) {
const rect = new Rect(
gl,
Math.random() * 1.6 - 0.8,
Math.random() * 1.6 - 0.8
)
rect.speed = Math.random() * 0.01 + 0.001
rects[i] = rect
}
let time = new Date()
!(function render() {
const newTime = new Date()
console.log(newTime - time)
time = newTime
gl.clear(gl.COLOR_BUFFER_BIT)
rects.forEach((rect) => {
rect.ani()
rect.draw()
})
requestAnimationFrame(render)
})()
function initShaders(gl, vsSource, fsSource) {
const program = gl.createProgram()
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
gl.useProgram(program)
gl.program = program
return true
}
function createProgram(gl, vsSource, fsSource) {
const program = gl.createProgram()
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
return program
}
function loadShader(gl, type, source) {
const shader = gl.createShader(type)
gl.shaderSource(shader, source)
gl.compileShader(shader)
return shader
}
</script>
</body>
</html>
效果如下:
console.log(newTime - time) 打印结果如下:
初次绘图用时4903,之后的刷新频率在7185-12630之间,平均值可以取10000。
4-结果分析
下图是我们之前的测试结果:
由此可知:
- canvas2d 在绘制简单图形的时候,速度最快。
- wasm 要比canvas2d 慢一倍。
- WebGL 最慢,且极慢。
对于这个结果,我考虑到以下可能原因:
- canvas2d之所以最快,是它在绘制简单的二维图形,或者开发简单的二维项目的时候,具备主场优势,因为canvas2d 本身就是为二维而生的。
- wasm 的编译和执行速度快,我是深信不疑的,毕竟是Google出品。但它之所以比原生canvas2d 慢,我想它应该还没有发挥出自己的优势,至于如何在二维项目中发挥其最大优势,还有待研究。
- WebGL 最慢的原因是它擅长GPU并行渲染,而在渲染简单图形的时候,因为它要走一些复杂逻辑,比如着色器与js间的数据传输,反而渲染速度要比原生canvas 慢很多很多。
总结
最后,我个人认为,若只是开发简单的二维项目,不涉及复杂的光栅化,或者大量的逐像素操作,直接使用原生canvas2d 开发即可,因为适合的才是最好的。
下一章,我会在逐像素操作方面,对比一下canvas2d、wasm和WebGL的渲染速度。