
在本教程中,我将向您展示如何使用
OffscreenCanvas
将用于处理WebGL和
Three.js的所有代码放入一个单独的Web Worker线程中。 这加快了站点的工作速度,并且在薄弱的设备上,在页面加载过程中消失了。
这篇文章基于个人经验,当我在
网站上 添加旋转3D地球时,
Google Lighthouse的生产力提高了5个点,这对于轻松炫耀而言实在太多了。
问题
Three.js隐藏了很多复杂的WebGL问题,但代价不菲-该库为您的JS版本增加了
563 KB的浏览器(并且该库的体系结构不允许trichashing有效地工作)。
有人说图片通常重500 KB,这是非常错误的。 脚本的每个KB在性能上都比映像的KB强大得多。 为了使站点速度更快,您不仅需要考虑通道宽度和延迟时间,还需要考虑计算机CPU处理文件的操作时间。 在电话和性能较差的笔记本电脑上,处理可能需要比加载更长的时间。
170K JS处理需要3.5秒,而170K图像需要0.1秒-Eddie Osmani虽然浏览器将执行500 KB Three.js,但将阻止主页流,并且用户将看到界面混乱。
网络工作者和屏幕外画布
长期以来,我们一直有一个解决方案,在长期运行JS的过程中,不要消除这种麻烦-网络工作者在单独的线程中运行代码。
因此,与Web Worker一起工作不会变成多线程编程的难题,因此Web Worker无法访问DOM。 只有主线程可用于HTML页面。 但是如何在不访问DOM的情况下启动Three.js,而DOM需要直接访问
<canvas>
?
为此,有
OffscreenCanvas-它允许您将
<canvas>
传递给Web Worker。 为了不打开多线程地狱的大门,在转移之后,主线程将失去对该
<canvas>
访问权限-只有一个线程可以使用它。
看来我们已接近目标,但事实证明只有Chrome支持
OffscreenCanvas
。
根据 `` 我可以使用''的规定, OffscreenCanvas支持2019年4月但是即使在这里,面对Web开发人员的主要敌人,即浏览器支持,我们也不应放弃。 我们聚在一起,找到难题的最后一个要素-这是“逐步改进”的理想案例。 在Chrome和以后的浏览器中,我们将删除这些限制,其他浏览器将像以前一样运行。
结果,我们将需要编写一个文件,该文件可以同时在两种不同的环境中工作-在Web worker和常规主JS流中。
解决方案
为了隐藏这些漏洞,我制作了一个小小的400字节(!)
的画布外 JS库。 在示例中,代码将使用它,但是我将告诉您它在“幕后”的工作方式。
让我们从安装库开始:
npm install offscreen-canvas
我们需要为网络工作者提供单独的JS文件-在Webpack或Parcel中创建单独的程序集文件:
entry: { 'app': './src/app.js', + 'webgl-worker': './src/webgl-worker.js' }
由于缓存破坏器,收集器将在部署过程中不断更改文件名-我们将需要使用
preload标记以HTML编写文件名。 这里的示例将是抽象的,因为实际的代码将在很大程度上取决于程序集的功能。
<link type="preload" as="script" href="./webgl-worker.js"> </head>
现在,我们需要获取
<canvas>
的DOM节点以及主JS文件中preload标签的内容。
import createWorker from 'offscreen-canvas/create-worker' const workerUrl = document.querySelector('[rel=preload][as=script]').href const canvas = document.querySelector('canvas') const worker = createWorker(canvas, workerUrl)
如果有
createWorker
,则
canvas.transferControlToOffscreen
JS文件加载到Web Worker中。 在没有这种方法的情况下-就像常规的
<script>
。
为工作人员创建以下
webgl-worker.js
:
import insideWorker from 'offscreen-canvas/inside-worker' const worker = insideWorker(e => { if (e.data.canvas) {
insideWorker
检查是否已将其加载到Web Worker中。 根据环境,它将使用主线程启动不同的通信系统。
对于主线程中的每个新消息,该库将运行传递给
insideWorker
的函数。 加载后,
createWorker
将立即发送第一条消息
{ canvas, width, height }
以在
<canvas>
上绘制第一帧。
+ import { + WebGLRenderer, Scene, PerspectiveCamera, AmbientLight, + Mesh, SphereGeometry, MeshPhongMaterial + } from 'three' import insideWorker from 'offscreen-canvas/inside-worker' + const scene = new Scene() + const camera = new PerspectiveCamera(45, 1, 0.01, 1000) + scene.add(new AmbientLight(0x909090)) + + let sphere = new Mesh( + new SphereGeometry(0.5, 64, 64), + new MeshPhongMaterial() + ) + scene.add(sphere) + + let renderer + function render () { + renderer.render(scene, camera) + } const worker = insideWorker(e => { if (e.data.canvas) { + // canvas - — , Three.js + if (!canvas.style) canvas.style = { width, height } + renderer = new WebGLRenderer({ canvas, antialias: true }) + renderer.setPixelRatio(pixelRatio) + renderer.setSize(width, height) + + render() } })
在将Three.js的旧代码移植到Web工作者时,您可能会看到错误,因为Web工作者没有DOM API。 例如,没有用于加载SVG纹理的
document.createElement
。 因此,有时我们将需要在Web Worker和常规脚本中使用不同的加载程序。 要检查环境的类型,我们有
worker.isWorker
:
renderer.setPixelRatio(pixelRatio) renderer.setSize(width, height) + const loader = worker.isWorker ? new ImageBitmapLoader() : new ImageLoader() + loader.load('/texture.png', mapImage => { + sphere.material.map = new CanvasTexture(mapImage) + render() + }) render()
我们画了第一帧。 但是大多数WebGL场景应该响应用户的操作。 例如,当光标移动时旋转摄像机,或者在调整窗口大小时绘制框架。 不幸的是,Web Worker无法收听DOM事件。 我们需要在主流中收听它们,并将消息发送给Web Worker。
import createWorker from 'offscreen-canvas/create-worker' const workerUrl = document.querySelector('[rel=preload][as=script]').href const canvas = document.querySelector('canvas') const worker = createWorker(canvas, workerUrl) + window.addEventListener('resize', () => { + worker.post({ + type: 'resize', width: canvas.clientWidth, height: canvas.clientHeight + }) + })
const worker = insideWorker(e => { if (e.data.canvas) { if (!canvas.style) canvas.style = { width, height } renderer = new WebGLRenderer({ canvas, antialias: true }) renderer.setPixelRatio(pixelRatio) renderer.setSize(width, height) const loader = worker.isWorker ? new ImageBitmapLoader() : new ImageLoader() loader.load('/texture.png', mapImage => { sphere.material.map = new CanvasTexture(mapImage) render() }) render() - } + } else if (e.data.type
结果
使用
OffscreenCanvas
我击败了网站上的带状装饰,并在Google Lighthouse上获得了100%的积分。 即使没有
OffscreenCanvas
支持,WebGL也可以在所有浏览器中使用。
您可以查看
实时站点以及
主线程或
worker的
源代码 。
借助OffscreenCanvas,Google Lighthouse眼镜从95升至100