В этом руководстве я расскажу как с помощью OffscreenCanvas
мне удалось вынести весь код работы с WebGL и Three.js в отдельный поток веб-воркера. Это ускорило работу сайта и на слабых устройствах исчезли фризы во время загрузки страницы.
Статья основана на личном опыте, когда я добавил вращающуюся 3D-землю на свой сайт и это забрало 5 очков производительности в Google Lighthouse — слишком много для лёгких понтов.
<canvas>
?<canvas>
в веб-воркер. Чтобы не открывать врата многопоточного ада, после передачи, основной поток теряет доступ к этому <canvas>
— только один поток будет работать с ним.OffscreenCanvas
.npm install offscreen-canvas
entry: {
'app': './src/app.js',
+ 'webgl-worker': './src/webgl-worker.js'
}
<link type="preload" as="script" href="./webgl-worker.js">
</head>
<canvas>
и содержимое 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-файл в веб-воркер. А при отсутствии этого метода — как обычный <script>
.webgl-worker.js
для воркера:import insideWorker from 'offscreen-canvas/inside-worker'
const worker = insideWorker(e => {
if (e.data.canvas) {
// Тут мы будем рисовать сцену на <canvas>
}
})
insideWorker
проверяет, был ли он загружен внутри веб-воркера. В зависимости от окружения он запустит разные системы связи с основным потоком.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()
}
})
document.createElement
для загрузкии SVG-текстур. Так что, нам будут иногда нужны разные загрузчики в веб-воркере и внутри обычного скрипта. Для проверки типа окружения у нас есть 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()
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 === 'resize') {
+ renderer.setSize(width, height)
+ render()
+ }
})
OffscreenCanvas
я победил фризы на моём сайте и получил 100% очков в Google Lighthouse. И WebGL работает во всех браузерах, даже без поддержки OffscreenCanvas
.К сожалению, не доступен сервер mySQL