Контакты
Москва
Серебряническая наб., 29
+7 (495) 108-24-49
Кемерово
Ноградская, 5, офис 404
+7 (3842) 65-04-90
Digital-агентство
Мэйк

Three.js: оптимизация размера бандла

Ускорить загрузку сайта можно при помощи различных оптимизаций. Одна из них — уменьшение размера бандлов с кодом на JavaScript. В этой статье мы рассмотрим такой подход на примере библиотеки three.js. А также разберём интересный механизм, позволяющий современным браузерам быстрее обрабатывать скрипты.

Статья является продолжением большого обзора по WebGL, который мы начали в сентябрьском лектории.

 

Влияние размера бандла на скорость загрузки сайта

Размеры «бандлов» с кодом на языке JavaScript имеют огромное значение при оптимизации скорости загрузки сайта. Чем меньше будут эти файлы, тем лучше. Здесь важно знать следующий факт: браузер быстрее обработает изображение объёмом 100 KB, чем те же 100 КВ скриптов. Это связано с тем, что браузеру помимо скачивания необходимо также провести синтаксический анализ кода, оптимизировать его, скомпилировать в байт-код и запустить последний на выполнение.

Процесс синтаксического анализа и компиляции JavaScript-кода в браузерах, использующих движок V8 (Google Chrome один из них), оптимизирован за счет улучшенной потоковой загрузки скриптов. Современные браузеры обрабатывают скрипты параллельно, распределяя их на несколько независимых «фоновых» потоков для элементов <script> с атрибутами async/defer.

Сравните скорость загрузки страницы в Facebook в браузерах Google Chrome версий 51 (2016 год) и 90 (2021 год). Невооруженным глазом видно, что в версии браузера 2021 года страница загружается заметно быстрее:

Сравнение скорости загрузки страницы в Chrome версий 51 и 90

Слева браузер Google Chrome версии 2016 года, справа — 2021.

Пример с Facebook показателен тем, что на страницах этой социальной сети грузится большое количество ресурсов. При загрузке страницы из примера инициируется 137 запросов на небольшие JavaScript «бандлы»; их суммарный объем составляет 8 MB, или 57% от всех загружаемых на странице данных. Благодаря распараллеленной потоковой загрузке скриптов они обрабатываются браузером очень быстро, без блокировки основного потока.

Вывод здесь простой: если вы хотите значительно оптимизировать скорость загрузки сайта, то работайте со скриптами. При этом затраты на синтаксический анализ и компиляцию в байт-код можно довольно легко нивелировать путем разбиения «бандла» на множество небольших, не превышающих 50 KB каждый.

Размер бандла three.js

Когда мы говорим про WebGL, то практически всегда рассматриваем его в контексте популярной библиотеки — three.js, babylon.js, regl и т. д. Эти библиотеки инкапсулируют в себя всю сложную работу с «низкоуровневым» API WebGL, предоставляя гораздо более простые и удобные в использовании абстракции.

Библиотека three.js является самой древней (первый релиз состоялся в 2010 году) и наиболее мощной по функционалу. Но как и в случае других библиотек, для работы зачастую достаточно лишь небольшой части её инструментария. Например, вам скорее всего никогда не потребуется реализация геометрии торического узла. Или класс вроде ShadowMaterial — он используется для создания материалов, которые могут «принимать» тени от других объектов, в остальном являясь полностью прозрачными.

При помощи сервиса BundlePhobia можно заранее оценить объем такого «лишнего» кода. В случае библиотеки three.js (пакет three) картина получается следующей:

Анализ библиотеки three.js в сервисе bundlephobia.com

Возьмём для сравнения не менее популярную библиотеку babylon.js. Для пакета, содержащего только её «ядро» (@babylonjs/core) результат получается ещё страшнее:

Анализ библиотеки babylon.js в сервисе bundlephobia.com

Если вы захотите использовать данные библиотеки в своем проекте, вам придётся иметь дело с дополнительными 146,9 KB от three.js или 698,7 KB (!) от babylon.js соответственно (без корректно настроенного gzip-сжатия на сервере ситуация будет ещё хуже). Если учесть, что в реальных проектах дополнительно используются и другие библиотеки/фреймворки, то ситуация становится весьма удручающей.

Как удалить «мёртвый» код из бандла three.js

К счастью, в современных сборщиках модулей вроде webpack существует механизм, позволяющий избежать добавления «мёртвого» (неиспользуемого) кода из сторонних библиотек. Данный механизм называется tree shaking и наиболее эффективно он работает в случае использования модулей, введенных в стандарте ECMAScript 6.0.

Кодовая база обеих рассмотренных выше библиотек была переписана их авторами из «монолитного» состояния с введением поддержки модулей ES6. Появляется закономерный вопрос: «т. е. теперь я могу не заботиться о „лишнем“ коде, и мой сборщик автоматически будет его игнорировать?». Ответом на него будет «Нет».

Tree shaking модулей three.js

Рассмотрим пару примеров оптимизации размера кода в контексте библиотеки three.js. Эта библиотека по своему функционалу не уступает babylon.js, при этом размер её «бандла» почти в пять раз меньше.

Так почему же нельзя рассчитывать на то, что сборщик будет всегда пропускать неиспользуемые вами модули из сторонних библиотек?

Во-первых, можно очень легко загубить всю идею tree shaking, если следовать примеру из README.md репозитория three.js:

import * as THREE from './js/three.module.js';

— приведенный выше код подключит абсолютно всё содержимое библиотеки и локально назначит пространство имён THREE, и таким образом «бандлер» уже не сможет явно определить, что из библиотеки three.js вы действительно используете, а что — нет.

Но на этом всё, естественно, не заканчивается. Рассмотрим процесс tree shaking на простом примере с официального сайта библиотеки:

Пример с вращающимся кубом на three.js

Перепишем код этого примера с использованием 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;

Выводы

  • При оптимизации скорости загрузки сайта уделяйте особое внимание скорости скачивания скриптов и процессорному времени, которое тратится на их выполнение.
  • Стандартный механизм tree shaking сборщика модулей webpack плохо работает с «импортами» из модуля three библиотеки three.js.
  • Для решения проблемы с tree shaking указывайте явно пути до ES6-модулей библиотеки или используйте плагин @vxna/optimize-three-webpack-plugin.
Даниил Филиппов

front-end разработчик

Почитать еще на эту тему

Обсудить