Tree shaking

Tree shaking — это процесс удаления неиспользуемого кода из итоговой сборки JavaScript-приложения. Современные сборщики, такие как Webpack, Rollup, Vite и esbuild, анализируют зависимости проекта и исключают из финального бандла функции, классы и модули, которые не используются в приложении.

Для веб-картографических приложений размер бандла имеет особое значение. Mapbox GL JS содержит значительный объем функциональности: работу со стилями, слоями, геометрией, событиями, анимацией, источниками данных, поддержкой WebGL и множеством вспомогательных механизмов. Поэтому вопросы оптимизации размера итоговой сборки становятся важной частью разработки.


Назначение Tree Shaking

Основные цели tree shaking:

  • уменьшение размера JavaScript-файлов;
  • ускорение загрузки приложения;
  • сокращение времени парсинга и выполнения кода браузером;
  • повышение производительности на мобильных устройствах;
  • уменьшение сетевого трафика.

В крупных GIS-системах и веб-картах экономия даже нескольких сотен килобайт может существенно повлиять на скорость первого отображения интерфейса.


Принцип работы

Tree shaking основан на статическом анализе импортов и экспортов ES Modules.

Например:

// utils.js
export function formatCoordinates() {
    // ...
}

export function calculateDistance() {
    // ...
}

Использование:

import { formatCoordinates } from './utils.js';

formatCoordinates();

Сборщик определяет, что функция calculateDistance() нигде не используется, и удаляет её из итогового бандла.

Схематично процесс выглядит следующим образом:

  1. Анализируются импорты.
  2. Строится граф зависимостей.
  3. Выявляются неиспользуемые экспорты.
  4. Неиспользуемый код удаляется.
  5. Производится минификация оставшегося кода.

Особенности Mapbox GL JS

Mapbox GL JS исторически разрабатывался как монолитная библиотека. Большая часть функциональности тесно связана между собой:

  • рендеринг карты;
  • управление слоями;
  • обработка тайлов;
  • система событий;
  • работа с источниками данных;
  • WebGL-движок.

По этой причине классический tree shaking в отношении самой библиотеки работает ограниченно.

Типичное подключение выглядит так:

import mapboxgl from 'mapbox-gl';

После такого импорта сборщик рассматривает библиотеку как единый модуль и включает значительную часть её кода в сборку.


Почему Tree Shaking ограничен в Mapbox GL JS

Для эффективного tree shaking необходимы следующие условия:

Использование ES Modules

Сборщик должен видеть структуру экспортов:

export function addLayer() {}
export function removeLayer() {}

Отсутствие побочных эффектов

Код не должен выполнять действия при импорте:

console.log('module loaded');

или

window.globalConfig = {};

Независимые модули

Функциональность должна быть разделена на отдельные части.

Mapbox GL JS содержит множество внутренних взаимосвязей:

Map
 ├── Style
 ├── SourceCache
 ├── Tile
 ├── Transform
 ├── Painter
 └── Evented

Удаление одного компонента часто невозможно без нарушения работы остальных.


Tree Shaking пользовательского кода вокруг Mapbox

Хотя сама библиотека оптимизируется ограниченно, tree shaking прекрасно работает для окружающего приложения.

Например:

// mapHelpers.js

export function createMap() {
    // ...
}

export function exportMapImage() {
    // ...
}

export function calculateBounds() {
    // ...
}

Использование:

import { createMap } from './mapHelpers.js';

createMap();

В итоговый бандл попадет только функция createMap().


Правильный стиль импортов

Для максимальной эффективности необходимо использовать именованные импорты там, где это возможно.

Предпочтительно:

import { calculateDistance } from './geoUtils.js';

Менее эффективно:

import * as GeoUtils from './geoUtils.js';

Во втором случае сборщику сложнее определить реально используемые части модуля.


Влияние CommonJS

Tree shaking плохо работает с CommonJS-модулями.

Пример:

const geoUtils = require('./geoUtils');

или

module.exports = {
    calculateDistance,
    calculateArea
};

Подобная структура не предоставляет достаточно информации для статического анализа.

Современные проекты с Mapbox GL JS обычно используют ES Modules:

import mapboxgl from 'mapbox-gl';

что значительно улучшает возможности оптимизации.


Tree Shaking и Vite

Vite использует Rollup для production-сборки.

Пример настройки:

import { defineConfig } from 'vite';

export default defineConfig({
    build: {
        sourcemap: false
    }
});

Во время сборки Rollup автоматически:

  • анализирует импорты;
  • удаляет неиспользуемые экспорты;
  • объединяет модули;
  • выполняет минификацию.

Запуск:

npm run build

После сборки можно заметить существенное сокращение размера собственного кода проекта.


Tree Shaking и Webpack

Webpack поддерживает tree shaking начиная с версии 2.

Пример конфигурации:

module.exports = {
    mode: 'production'
};

Production-режим автоматически включает:

  • анализ используемых экспортов;
  • минификацию;
  • dead code elimination.

Для дополнительного контроля используется параметр:

optimization: {
    usedExports: true
}

Поле sideEffects

В package.json можно указать отсутствие побочных эффектов:

{
    "sideEffects": false
}

Это позволяет сборщику агрессивнее удалять неиспользуемые модули.

Пример:

import { createMap } from './map.js';

Если другие функции из файла не используются, они будут исключены из бандла.


Осторожность при использовании sideEffects

Нельзя указывать:

{
    "sideEffects": false
}

если модуль выполняет действия при загрузке.

Например:

mapboxgl.accessToken = TOKEN;

или

import './globalStyles.css';

В подобных случаях сборщик может ошибочно удалить необходимый код.

Корректный вариант:

{
    "sideEffects": [
        "*.css"
    ]
}

Динамический импорт как дополнение к Tree Shaking

Tree shaking часто используется совместно с code splitting.

Вместо:

import mapboxgl from 'mapbox-gl';

можно применять динамическую загрузку:

const mapboxgl = await import('mapbox-gl');

или:

button.addEventListener('click', async () => {
    const mapboxgl = await import('mapbox-gl');

    createMap(mapboxgl.default);
});

В этом случае библиотека загружается только при необходимости.


Анализ размера бандла

Для оценки эффективности tree shaking применяются специальные инструменты.

Webpack Bundle Analyzer

Установка:

npm install webpack-bundle-analyzer --save-dev

Подключение:

const BundleAnalyzerPlugin =
    require('webpack-bundle-analyzer')
        .BundleAnalyzerPlugin;

module.exports = {
    plugins: [
        new BundleAnalyzerPlugin()
    ]
};

После сборки открывается интерактивная диаграмма содержимого бандла.

Rollup Visualizer

Установка:

npm install rollup-plugin-visualizer --save-dev

Использование:

import { visualizer } from 'rollup-plugin-visualizer';

export default {
    plugins: [
        visualizer()
    ]
};

Это позволяет увидеть, какую долю занимает Mapbox GL JS и какие модули проекта остаются после tree shaking.


Распространённые ошибки

Импорт всего модуля

import * as Helpers from './helpers.js';

Даже если используется одна функция:

Helpers.calculateDistance();

лучше писать:

import { calculateDistance } from './helpers.js';

Реэкспорт крупных наборов модулей

export * from './geo.js';
export * from './map.js';
export * from './layers.js';
export * from './styles.js';

Подобная структура усложняет анализ зависимостей.

Более эффективен явный экспорт:

export { createMap } from './map.js';
export { calculateDistance } from './geo.js';

Побочные эффекты в модулях

Плохо:

initializeApplication();

export function createMap() {}

Лучше:

export function initializeApplication() {}

export function createMap() {}

Инициализация вызывается явно:

initializeApplication();

Архитектура проекта для максимального Tree Shaking

Хорошо организованный проект с Mapbox GL JS обычно разделяет функциональность по модулям.

Структура:

src/
├── map/
│   ├── createMap.js
│   ├── controls.js
│   ├── layers.js
│   └── sources.js
│
├── geo/
│   ├── distance.js
│   ├── bounds.js
│   └── projection.js
│
└── ui/
    ├── sidebar.js
    ├── popup.js
    └── toolbar.js

Использование:

import { createMap } from './map/createMap.js';
import { createPopup } from './ui/popup.js';

Неиспользуемые части автоматически исключаются сборщиком.


Tree Shaking и плагины Mapbox

Дополнительные библиотеки экосистемы Mapbox могут влиять на размер бандла:

  • Mapbox GL Draw;
  • Geocoder;
  • Compare;
  • Directions;
  • различные пользовательские плагины.

Вместо постоянного импорта:

import MapboxDraw from '@mapbox/mapbox-gl-draw';

можно применять ленивую загрузку:

const { default: MapboxDraw } =
    await import('@mapbox/mapbox-gl-draw');

Это уменьшает первоначальный объем загружаемого кода.


Сравнение подходов

Подход Размер начальной загрузки Эффективность
Импорт всего приложения Высокий Низкая
Tree shaking Средний Высокая
Tree shaking + code splitting Низкий Очень высокая
Tree shaking + динамический импорт Mapbox-плагинов Очень низкий Максимальная

Практические рекомендации

Для проектов на Mapbox GL JS наиболее эффективной считается следующая стратегия:

  1. Использовать ES Modules.
  2. Избегать CommonJS.
  3. Разделять код на небольшие независимые модули.
  4. Применять именованные импорты.
  5. Исключать побочные эффекты из модулей.
  6. Настраивать поле sideEffects.
  7. Использовать динамический импорт тяжёлых компонентов.
  8. Анализировать бандл через Bundle Analyzer или Visualizer.
  9. Загружать плагины Mapbox по требованию.
  10. Регулярно контролировать размер production-сборки.

В современных приложениях с Mapbox GL JS tree shaking редко приводит к значительному уменьшению размера самой картографической библиотеки, однако позволяет существенно сократить объем пользовательского кода и вспомогательных модулей. В сочетании с code splitting и динамическими импортами этот механизм становится одним из ключевых инструментов оптимизации производительности картографических веб-приложений.