Create: August 14, 2024
Last Modify: Aug 14, 2024

一次失败的 WebAssembly 尝试

背景: 项目中有一个非常耗时的操作,扩大选区,即找到图片边缘的部分,以边缘的每个点为圆心画圆,以此平滑扩大选区;但是随着选区变大,带来的耗时非常恐怖,大到200+ms; 遂尝试用 WebAssembly 试试,看看耗时能否降低。

尝试:

用 go 重写了这块代码

func main() {
	jsProcess := js.FuncOf(Process)
	js.Global().Set("process", jsProcess)

	defer jsProcess.Release()

	select {}
}


type process struct {
	data js.Value
	img  js.Value
}

func Process(this js.Value, args []js.Value) interface{} {
	imgSrc := args[0]
	storkeWidth := args[1]
	jsCallback := args[2]

	if storkeWidth.Int() == 0 {
		return js.ValueOf(process{data: js.ValueOf(nil), img: imgSrc})
	}

	document := js.Global().Get("document")

	canvas := document.Call("createElement", "canvas")
	ctx := canvas.Call("getContext", "2d")
	fillColor := js.ValueOf("rgba(116, 90, 241, .5)")

	img := document.Call("createElement", "img")
	img.Set("crossorigin", "anonymous")
	img.Set("src", imgSrc)

	var onloadCallback js.Func

	onloadCallback = js.FuncOf(func(this js.Value, _ []js.Value) interface{} {
		canvas.Set("width", img.Get("width"))
		canvas.Set("height", img.Get("height"))

		ctx.Call("drawImage", img, 0, 0)

		imageData := ctx.Call("getImageData", 0, 0, img.Get("width").Int(), img.Get("height").Int())
		data := imageData.Get("data")

		boundaries := []int{}
		length := data.Get("length").Int()

        // tag1

		for i := 0; i < length; i += 4 {
			if data.Index(i+3).Int() != 0 {
				boundaries = append(boundaries, i)
			}
		}

        // tag1 end

		minX := imageData.Get("width").Int()
		maxX := 0
		maxY := 0
		var w = img.Get("width").Int()

        // tag2

		for _, v := range boundaries {
			x := (v / 4) % w
			y := (v / 4 / w)
			if x < minX {
				minX = x
			}
			if x > maxX {
				maxX = x
			}
			if y > maxY {
				maxY = y
			}

			FillCriclePath(ctx, js.ValueOf(x), js.ValueOf(y), storkeWidth, fillColor)
		}

        // tag2 end

		newImageData := ctx.Call("getImageData", 0, 0, canvas.Get("width").Int(), canvas.Get("height").Int())

		res := process{data: newImageData, img: canvas.Call("toDataURL")}

		resMap := map[string]interface{}{
			"data": res.data,
			"img":  res.img,
		}

		jsCallback.Invoke(js.ValueOf(resMap))

		onloadCallback.Release()
		return js.ValueOf(nil)
	})

	img.Set("onload", onloadCallback)

	return js.ValueOf(nil)
}

func FillCriclePath(ctx js.Value, x js.Value, y js.Value, N js.Value, color js.Value) interface{} {
	path := GetCriclePath(x, y, N)
	ctx.Set("fillStyle", color)
	ctx.Call("fill", path)
	return nil
}

func GetCriclePath(x js.Value, y js.Value, radius js.Value) interface{} {
	path := js.Global().Get("Path2D").New()
	path.Call("arc", x, y, radius, js.ValueOf(0), js.Global().Get("Math").Get("PI").Float()*2)
	return path
}

然后编译成wasm

GOOS=js GOARCH=wasm go build -o process.wasm

然后满怀期待的放到js中

const go = new Go();
WebAssembly.instantiateStreaming(
    fetch('/public/assets/main.wasm'),
    go.importObject
).then((result) => {
    go.run(result.instance);
});

一看耗时

wasm 耗时

再看原 js 耗时

js 耗时

:)

在go中加了时间同级,主要耗时点在 tag1 和 tag2 中;tag2 很耗时也好理解,每个循环都要操作dom画圆,但是 tag1 耗时就堪比 js 整个逻辑操作了,就很难理解

tag1 耗时

待我找找答案

© 2019