Статья является продолжением большого обзора по WebGL, который мы начали в сентябрьском лектории.
Размеры «бандлов» с кодом на языке JavaScript имеют огромное значение при оптимизации скорости загрузки сайта. Чем меньше будут эти файлы, тем лучше. Здесь важно знать следующий факт: браузер быстрее обработает изображение объёмом 100 KB, чем те же 100 КВ скриптов. Это связано с тем, что браузеру помимо скачивания необходимо также провести синтаксический анализ кода, оптимизировать его, скомпилировать в байт-код и запустить последний на выполнение.
Процесс синтаксического анализа и компиляции JavaScript-кода в браузерах, использующих движок V8 (Google Chrome один из них), оптимизирован за счет улучшенной потоковой загрузки скриптов. Современные браузеры обрабатывают скрипты параллельно, распределяя их на несколько независимых «фоновых» потоков для элементов <script> с атрибутами async/defer.
Сравните скорость загрузки страницы в Facebook в браузерах Google Chrome версий 51 (2016 год) и 90 (2021 год). Невооруженным глазом видно, что в версии браузера 2021 года страница загружается заметно быстрее:
Пример с Facebook показателен тем, что на страницах этой социальной сети грузится большое количество ресурсов. При загрузке страницы из примера инициируется 137 запросов на небольшие JavaScript «бандлы»; их суммарный объем составляет 8 MB, или 57% от всех загружаемых на странице данных. Благодаря распараллеленной потоковой загрузке скриптов они обрабатываются браузером очень быстро, без блокировки основного потока.
Вывод здесь простой: если вы хотите значительно оптимизировать скорость загрузки сайта, то работайте со скриптами. При этом затраты на синтаксический анализ и компиляцию в байт-код можно довольно легко нивелировать путем разбиения «бандла» на множество небольших, не превышающих 50 KB каждый.
Когда мы говорим про WebGL, то практически всегда рассматриваем его в контексте популярной библиотеки — three.js, babylon.js, regl и т. д. Эти библиотеки инкапсулируют в себя всю сложную работу с «низкоуровневым» API WebGL, предоставляя гораздо более простые и удобные в использовании абстракции.
Библиотека three.js является самой древней (первый релиз состоялся в 2010 году) и наиболее мощной по функционалу. Но как и в случае других библиотек, для работы зачастую достаточно лишь небольшой части её инструментария. Например, вам скорее всего никогда не потребуется реализация геометрии торического узла. Или класс вроде ShadowMaterial — он используется для создания материалов, которые могут «принимать» тени от других объектов, в остальном являясь полностью прозрачными.
При помощи сервиса BundlePhobia можно заранее оценить объем такого «лишнего» кода. В случае библиотеки three.js (пакет three) картина получается следующей:
Возьмём для сравнения не менее популярную библиотеку babylon.js. Для пакета, содержащего только её «ядро» (@babylonjs/core) результат получается ещё страшнее:
Если вы захотите использовать данные библиотеки в своем проекте, вам придётся иметь дело с дополнительными 146,9 KB от three.js или 698,7 KB (!) от babylon.js соответственно (без корректно настроенного gzip-сжатия на сервере ситуация будет ещё хуже). Если учесть, что в реальных проектах дополнительно используются и другие библиотеки/фреймворки, то ситуация становится весьма удручающей.
К счастью, в современных сборщиках модулей вроде webpack существует механизм, позволяющий избежать добавления «мёртвого» (неиспользуемого) кода из сторонних библиотек. Данный механизм называется tree shaking и наиболее эффективно он работает в случае использования модулей, введенных в стандарте ECMAScript 6.0.
Кодовая база обеих рассмотренных выше библиотек была переписана их авторами из «монолитного» состояния с введением поддержки модулей ES6. Появляется закономерный вопрос: «т. е. теперь я могу не заботиться о „лишнем“ коде, и мой сборщик автоматически будет его игнорировать?». Ответом на него будет «Нет».
Рассмотрим пару примеров оптимизации размера кода в контексте библиотеки three.js. Эта библиотека по своему функционалу не уступает babylon.js, при этом размер её «бандла» почти в пять раз меньше.
Так почему же нельзя рассчитывать на то, что сборщик будет всегда пропускать неиспользуемые вами модули из сторонних библиотек?
Во-первых, можно очень легко загубить всю идею tree shaking, если следовать примеру из README.md репозитория three.js:
import * as THREE from './js/three.module.js';
— приведенный выше код подключит абсолютно всё содержимое библиотеки и локально назначит пространство имён THREE, и таким образом «бандлер» уже не сможет явно определить, что из библиотеки three.js вы действительно используете, а что — нет.
Но на этом всё, естественно, не заканчивается. Рассмотрим процесс tree shaking на простом примере с официального сайта библиотеки:
Перепишем код этого примера с использованием ES6 модулей:
import {
PerspectiveCamera,
Scene,
TextureLoader,
BoxGeometry,
MeshBasicMaterial,
Mesh,
WebGLRenderer,
} from 'three';
let camera, scene, renderer;
let mesh;
function init() {
camera = new PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
1,
1000,
);
camera.position.z = 400;
scene = new Scene();
const texture = new TextureLoader().load('textures/crate.gif');
const geometry = new BoxGeometry(200, 200, 200);
const material = new MeshBasicMaterial({ map: texture });
mesh = new Mesh(geometry, material);
scene.add(mesh);
renderer = new WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
mesh.rotation.x += 0.005;
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
init();
animate();
Код этого примера довольно простой, а из модуля three импортируются всего 7 классов. Можно ожидать, что итоговый «бандл» будет также небольшим за счет tree shaking (по крайней мере гораздо меньше, чем 608,3 KB).
Однако, если собрать приведенный выше код при помощи актуальной версии webpack (на момент написания этой статьи — 5.35.1), то на выходе, скорее всего, будет следующая картина:
asset bundle.min.js 507 KiB [emitted] [minimized] (name: main) 2 related assets
— довольно странно, что итоговый размер «бандла» меньше максимального всего на ≈100 KB, не правда ли? Отчасти это объясняется тем, что большинство классов рассматриваемой библиотеки тесно связаны между собой.
Например, класс WebGLRenderer создаёт в своем конструкторе объект класса WebXRManager, являющийся абстракцией для WebXR Device API (интерфейс, который вряд ли потребуется в большинстве приложений, использующих WebGL).
Истинная причина данной проблемы станет понятна, если посмотреть содержимое конфигурационного файла package.json библиотеки, а именно, поле module. Последнее используется «бандлерами» вроде webpack для определения местоположения «главного» ES6-модуля библиотеки, в котором подключаются все её составляющие части.
В случае three.js в значении данного поля указан путь до файла build/three.module.js, в котором находится... просто весь собранный код «ядра» библиотеки, без единого использования оператора export! Немудрено, что сборщику в такой ситуации будет уже очень трудно отличить «мёртвый» код от реально используемого.
Чтобы решить эту проблему, достаточно будет указать в коде нашего примера явный путь до основного ES6-модуля three.js в директории src:
@@ -6,7 +6,7 @@ import {
MeshBasicMaterial,
Mesh,
WebGLRenderer,
-} from 'three';
+} from 'three/src/Three';
let camera, scene, renderer;
let mesh;
Теперь если попробовать пересобрать проект, то можно увидеть следующее:
asset bundle.min.js 354 KiB [emitted] [minimized] (name: main) 1 related asset
— итоговый размер «бандла» уменьшился на целых 153 KB!
Есть ещё специальный плагин для webpack — @vxna/optimize-three-webpack-plugin. Он предоставляет целый набор «алиасов», которые можно применять при добавлении различных модулей three.js, включая модули «примеров» (three/examples/jsm).
С использованием данного плагина наш код можно переписать более красиво:
@@ -6,7 +6,7 @@ import {
MeshBasicMaterial,
Mesh,
WebGLRenderer,
-} from 'three/src/Three';
+} from '@three/core';
let camera, scene, renderer;
let mesh;