Tree shaking: как Parcel удаляет неиспользуемый код

Tree shaking — это процесс автоматического удаления неиспользуемого кода из итоговой сборки приложения. Название происходит от аналогии с деревом зависимостей: сборщик анализирует модули проекта и «стряхивает» ветви, которые нигде не используются.

В современных JavaScript-приложениях значительная часть кода импортируется из сторонних библиотек. Нередко разработчик использует лишь небольшую часть возможностей пакета, однако без механизма tree shaking в финальный бандл попадал бы весь модуль целиком. Это увеличивает размер файлов, время загрузки и потребление памяти.

Parcel поддерживает tree shaking из коробки и автоматически пытается исключить код, который не влияет на работу приложения.


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

В основе механизма лежит статический анализ импортов и экспортов ES-модулей (ESM).

Рассмотрим простой пример.

Файл math.js:

export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export function multiply(a, b) {
    return a * b;
}

Файл app.js:

import { add } from "./math.js";

console.log(add(2, 3));

Parcel анализирует граф зависимостей и определяет:

  • функция add() используется;
  • функции subtract() и multiply() нигде не импортируются.

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

Условно результат может выглядеть так:

function add(a, b) {
    return a + b;
}

console.log(add(2, 3));

Лишний код исчезает ещё на этапе сборки.


Почему используются именно ES-модули

Tree shaking возможен благодаря тому, что структура ES-модулей известна заранее.

Пример:

import { formatDate } from "./utils.js";

Сборщик сразу понимает:

  • какой модуль подключается;
  • какие сущности импортируются;
  • какие экспорты используются.

Для сравнения, CommonJS обладает динамической природой.

const utils = require("./utils");

const method = process.env.METHOD;

utils[method]();

Здесь невозможно заранее определить, какая функция будет вызвана во время выполнения программы.

Поэтому полноценный tree shaking работает именно с ES-модулями.


Как Parcel анализирует граф зависимостей

Во время сборки Parcel строит граф зависимостей проекта.

Например:

app.js
 ├── api.js
 ├── utils.js
 └── ui.js
      ├── button.js
      └── modal.js

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

Parcel определяет:

  1. Какие модули подключены.
  2. Какие экспорты используются.
  3. Какие экспорты не используются.
  4. Имеются ли побочные эффекты.

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


Используемые и неиспользуемые экспорты

Parcel отслеживает использование экспортируемых сущностей на уровне отдельных символов.

Файл:

export const PI = 3.14;

export function area(r) {
    return PI * r * r;
}

export function circumference(r) {
    return 2 * PI * r;
}

Импорт:

import { area } from "./geometry.js";

console.log(area(5));

Parcel понимает, что:

  • area() нужна;
  • PI нужна, так как используется внутри area();
  • circumference() не нужна.

В итоговый бандл попадут только необходимые элементы.


Удаление неиспользуемых импортов

Tree shaking работает и в обратную сторону.

Исходный код:

import {
    getUser,
    updateUser,
    deleteUser
} from "./api.js";

getUser();

Parcel определяет, что используются только:

getUser();

Остальные импорты не включаются в финальную сборку.


Побочные эффекты и их влияние

Главная сложность tree shaking связана с побочными эффектами (side effects).

Побочным эффектом считается код, который выполняет действия вне своей области видимости:

console.log("Module loaded");

или

window.theme = "dark";

или

document.body.classList.add("loaded");

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

Пример:

console.log("Analytics initialized");

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

Даже если track() не используется, удаление всего файла приведёт к исчезновению вывода в консоль.

Поэтому Parcel обязан сохранить выполнение такого модуля.


Полностью чистые модули

Идеальным кандидатом для tree shaking является модуль без побочных эффектов.

export function square(x) {
    return x * x;
}

export function cube(x) {
    return x * x * x;
}

Такой файл содержит только определения функций.

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


Поле sideEffects в package.json

Для более точной оптимизации используется свойство sideEffects.

Пример:

{
  "sideEffects": false
}

Такая запись сообщает сборщику:

Все файлы пакета не содержат побочных эффектов.

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


Частичное указание файлов с побочными эффектами

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

{
  "sideEffects": [
    "./src/polyfills.js",
    "./src/global.css"
  ]
}

В этом случае Parcel считает:

  • перечисленные файлы важными;
  • остальные файлы пригодными для tree shaking.

Пример работы с библиотекой

Предположим, существует пакет:

export function debounce() {}
export function throttle() {}
export function cloneDeep() {}
export function memoize() {}

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

import { debounce } from "my-utils";

При корректной настройке Parcel включит только:

function debounce() {
    // ...
}

Остальные функции будут удалены.


Tree shaking и импорт всего пространства имён

Следует учитывать особенности некоторых видов импорта.

Пример:

import * as utils from "./utils.js";

utils.format();

С точки зрения анализа такой код менее прозрачен.

Parcel может сохранить больше кода, поскольку объект utils потенциально способен использовать любые экспортируемые функции.

Более эффективный вариант:

import { format } from "./utils.js";

format();

Здесь сборщик точно знает, что нужен только один экспорт.


Реэкспорт модулей

Часто создаются файлы-агрегаторы.

export { add } from "./math.js";
export { format } from "./date.js";
export { validate } from "./validation.js";

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

import { add } from "./index.js";

Parcel способен проследить цепочку реэкспортов и удалить лишние зависимости.

В сборку попадёт только код, необходимый для работы функции add.


Tree shaking и классы

Механизм работает не только с функциями.

Файл:

export class User {}
export class Admin {}
export class Moderator {}

Импорт:

import { User } from "./models.js";

Parcel удалит неиспользуемые классы Admin и Moderator.


Tree shaking и константы

Аналогичный принцип действует для констант.

export const API_URL = "/api";
export const VERSION = "1.0";
export const BUILD_DATE = "2025-01-01";

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

import { API_URL } from "./config.js";

Неиспользуемые значения могут быть исключены из финального кода.


Взаимодействие с минификацией

Tree shaking и минификация — разные процессы.

Tree shaking

Удаляет:

function unused() {}

если функция не используется.

Минификация

Преобразует:

function calculatePrice(price, tax) {
    return price + tax;
}

в:

function a(b,c){return b+c}

Обычно Parcel сначала удаляет ненужный код, а затем минифицирует оставшийся.

Комбинация этих механизмов обеспечивает максимальное уменьшение размера бандла.


Dead Code Elimination

Tree shaking тесно связан с техникой Dead Code Elimination (DCE).

Она удаляет код, который гарантированно никогда не будет выполнен.

Пример:

if (false) {
    console.log("never");
}

После оптимизации:

// код удалён

Другой пример:

const DEBUG = false;

if (DEBUG) {
    logger.enable();
}

После подстановки констант Parcel способен убрать всю ветку условия.


Условная компиляция

Распространённый сценарий:

if (process.env.NODE_ENV !== "production") {
    console.log("Debug mode");
}

Во время production-сборки значение известно заранее.

После подстановки:

if (false) {
    console.log("Debug mode");
}

Затем неиспользуемая ветка удаляется механизмом DCE.


Tree shaking и динамические импорты

Динамические импорты работают несколько иначе.

const module = await import("./analytics.js");

Parcel выделяет такой модуль в отдельный чанк.

Однако внутри этого чанка tree shaking продолжает работать.

Если файл analytics.js экспортирует множество функций, но используются только некоторые из них, ненужный код всё равно может быть исключён.


Когда tree shaking не срабатывает

Существует ряд ситуаций, затрудняющих анализ.

Динамический доступ к свойствам

utils[methodName]();

Parcel не знает, какая функция понадобится во время выполнения.


Выполнение кода на верхнем уровне модуля

startAnalytics();

Такой код является побочным эффектом и препятствует удалению файла.


Изменение глобального состояния

window.config = {};

Подобные действия вынуждают сборщик сохранять модуль.


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

const lib = require("./lib");

Возможности tree shaking для таких модулей существенно ограничены.


Рекомендации по написанию кода

Для максимально эффективной работы Parcel полезно придерживаться следующих принципов:

Использование ES-модулей

import { helper } from "./helper.js";

вместо

const helper = require("./helper");

Минимизация побочных эффектов

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

export function init() {}

Вместо:

init();

export function init() {}

Явные импорты

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

import { format } from "./utils.js";

Вместо:

import * as utils from "./utils.js";

Корректное использование sideEffects

{
  "sideEffects": false
}

или

{
  "sideEffects": [
    "./src/polyfills.js"
  ]
}

Это помогает Parcel принимать более точные решения во время оптимизации.


Проверка результатов сборки

Production-сборка выполняется командой:

parcel build src/index.html

После завершения сборки в каталоге dist появляются оптимизированные файлы.

Для анализа размера бандлов обычно изучают:

  • размер итоговых файлов;
  • состав чанков;
  • количество включённых модулей;
  • наличие лишних зависимостей.

Сильное уменьшение размера сборки после перехода на ES-модули и корректной настройки sideEffects является одним из признаков успешной работы tree shaking.


Внутренний процесс оптимизации в Parcel

Во время production-сборки Parcel последовательно выполняет несколько этапов:

  1. Построение графа зависимостей.
  2. Анализ импортов и экспортов.
  3. Определение используемых символов.
  4. Выявление побочных эффектов.
  5. Исключение неиспользуемых модулей.
  6. Dead Code Elimination.
  7. Минификация кода.
  8. Формирование итоговых чанков.

В результате финальный бандл содержит только те части приложения и библиотек, которые действительно участвуют в работе программы. Это уменьшает объём передаваемых данных, ускоряет загрузку страниц и снижает расходы на обработку JavaScript в браузере.