在刚开始介绍WebGL时,我们就知道无论什么样的3D模型或者2D的形状,绘制的时候需要拆分成三角形顶点。有些模型可以提前准备好,使用时直接从服务端加载,但是另外一些情况数据可能没有分解为三角形或者需要进行额外的处理才能使用,这就需要在浏览器中进行加工处理。
比如用WebGL实现一些基础图形的绘制,就需要把图形分解为三角形,这个过程称为Triangulation,即三角形化。在图形元素过多、数据量较大的情况下,分解过程可能会比较耗时。在JavaScript同步处理可能会影响页面性能。因此可以考虑使用WebWorker做异步处理。对于数据可从服务端获取的,获取数据的逻辑也可以写在WebWorker中。
这里不再介绍什么是WebWorker以及如何使用它,但是需要在使用时考虑以下几个问题:
多少个WebWorker是合适的
一般情况下,可以通过
navigator.hardwareConcurrency
来获取用户设备最大并发数,通常这个数为4或者8。如果浏览器版本较旧或是非Chrome浏览器没有该接口,一般可取值为4。
既然系统最大并发数为4或者8,那么可以预留一个给主线程,其余由worker使用。不过通常主线程不会像worker处理数据那样较长时间处于忙的状态,这样也可以不给主线程预留,那么worker数量就是 navigator.hardwareConcurrency 的值。
网上还有方法是动态检测最合适的worker并发数,不过需要执行一些测试才能知晓。
WebWorker和主线程通信的代价
WebWorker所运行的环境与主线程的环境是隔离的,二者只可以通过postMessage方法和onmessage事件来发送和接收数据。需要注意的是在这个过程中数据是通过拷贝来传递的,也就是数据从一个上下文中拷贝到另一个上下文,这个过程叫做结构化克隆。如果数据量较大,那么内存上就会有一些开销,另外拷贝数据过程会导致 postMessage 耗时较长,同时 onmessage 事件处理函数中读取messageEvent.data 的耗时也较长,这都会影响页面性能。
解决方法有两种:
一是传递数据前使用 JSON.stringify 对数据做序列化处理,结构化克隆字符串的时间的要比结构化克隆一个对象省时的多。但是在接收数据时使用 JSON.parse 还原仍然有较多耗时。
另一个方法是利用postMessage的第二个参数指明哪些数据是按引用传递,这样可以避免克隆导致的耗时。目前只有像 ArrayBuffer、ImageBitmap 这样的数据类型支持按照引用传递,这些对象称作 Transferable Objects。
postMessage(message, transferList);
下面是一个按引用传递的例子:
postMessage({ data1: someData1, // Float32Array data2: someData2, // Float32Array data3: someSmallData // normal Object }, [someData1.buffer, someData2.buffer]);
postMessage 所传递的对象有三个属性,data1和data2是 Float32Array 类型,data3是普通的 JavaScript 对象,在第二个参数中指定 someData1.buffer 和 someData2.buffer 两个对象,它们的类型都是 ArrayBuffer,这样在传递 data1 和 data2 时就不会有克隆过程。性能得到极大的提升。
这里有一个简单的测试页面。在worker中创建了一个较大的数组,并传递给主线程。通过控制台输出传递数据和接收数据的耗时。
下面这张图显示了在 Macbook Pro Chrome 中三种方式下传递和接收数据的耗时,可见使用 transferList 收益最大。
但是在Safari和Firefox上测试又是另一番景象:
传递和接收数组的耗时远比Chrome上小得多,但是使用transferList的收益仍旧是最大的。
为了能够按引用传递数据,需要在worker中将数据提前处理成诸如 Float32Array 或者 Uint16Array 这样的类型数组,对于WebGL来说这天然就是合适的,WebGL中给Buffer提交的数据都是这些类型数组。如果是非WebGL场景,如果数据是纯数值类型依然可以事先封装为类型数组,之后再进行还原。
测试代码能贴出来吗,我实验接收数据慢得要死,传不传引用都一样
看到了,这个耗时只是从接收到信息开始计算的,我以为是从一个线程开始发送数据到另外一个接收到为止。
这里只分析传递数据环节的耗时,至于 worker 内部的耗时就和业务逻辑有关系了,不在讨论范围之内。