Современные веб-приложения могут достигать довольно больших размеров, особенно их JavaScript-часть. По данным HTTP Archive на середину 2018 года, средний размер передаваемого JavaScript-кода на мобильных устройствах составляет приблизительно 350 КБ. И это только размер передачи! JavaScript часто сжимается при передаче по сети, а это значит, что фактический объем JavaScript значительно увеличивается после декомпрессии браузером. Важно это отметить, поскольку с точки зрения обработки ресурсов сжатие не имеет значения. 900 КБ декомпрессированного JavaScript по-прежнему остаются 900 КБ для парсера и компилятора, даже если после сжатия они могут составлять примерно 300 КБ.
Обработка JavaScript — дорогостоящий процесс. В отличие от изображений, декодирование которых после загрузки занимает относительно немного времени, JavaScript необходимо проанализировать, скомпилировать и, наконец, выполнить. В пересчете на байт это делает JavaScript более затратным, чем другие типы ресурсов.

Хотя постоянно ведутся работы по повышению эффективности движков JavaScript , улучшение производительности JavaScript, как всегда, остается задачей для разработчиков.
С этой целью существуют методы повышения производительности JavaScript. Разделение кода — один из таких методов, который улучшает производительность за счет разделения JavaScript-кода приложения на фрагменты и предоставления этих фрагментов только тем маршрутам приложения, которые в них нуждаются.
Хотя этот метод работает, он не решает распространенную проблему приложений, активно использующих JavaScript, а именно включение кода, который никогда не используется. Метод "tree shaking" (удаление ненужных файлов) пытается решить эту проблему.
Что такое «тряска деревьев»?
«Удаление мертвого кода» (tree shaking) — это один из способов устранения мертвого кода. Термин стал популярным благодаря Rollup , но концепция удаления мертвого кода существует уже довольно давно. Эта концепция также нашла применение в webpack , что демонстрируется в этой статье на примере приложения.
Термин «tree shaking» происходит от представления вашего приложения и его зависимостей как древовидной структуры. Каждый узел в дереве представляет собой зависимость, обеспечивающую определенную функциональность вашего приложения. В современных приложениях эти зависимости подключаются с помощью статических операторов import , например, так:
// Import all the array utilities!
import arrayUtils from "array-utils";
Когда приложение молодое — своего рода саженец — у него может быть мало зависимостей. Оно также использует большинство, если не все, зависимости, которые вы добавляете. Однако по мере развития приложения может добавляться всё больше зависимостей. Вдобавок ко всему, старые зависимости перестают использоваться, но могут и не быть удалены из вашего кода. В результате приложение поставляется с большим количеством неиспользуемого JavaScript . «Удаление лишнего кода» (tree shaking) решает эту проблему, используя преимущества того, как статические операторы import подключают определённые части модулей ES6:
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
Разница между этим примером import и предыдущим заключается в том, что вместо импорта всего содержимого модуля "array-utils" (что может занять много кода), в этом примере импортируются только его отдельные части. В сборках для разработки это ничего не меняет, поскольку весь модуль импортируется независимо от настроек. В сборках для продакшена webpack можно настроить так, чтобы он "отбрасывал" экспорт из модулей ES6, которые не были явно импортированы, что уменьшит размер таких сборок. В этом руководстве вы узнаете, как это сделать!
Найти возможности потрясти дерево
В качестве иллюстрации доступно одностраничное приложение , демонстрирующее принцип работы функции «удаления деревьев» (tree shaking). Вы можете клонировать его и следовать инструкциям, если хотите, но в этом руководстве мы рассмотрим каждый шаг вместе, поэтому клонирование не обязательно (если только вам не нравится учиться на практике).
Приложение-демонстратор представляет собой базу данных с возможностью поиска гитарных эффектов. Вы вводите запрос, и появляется список эффектов.

Функционал приложения разделен на пакеты кода от сторонних разработчиков (например, Preact и Emotion ) и пакеты кода, специфичные для приложения (или «блоки», как их называет webpack):

Представленные на рисунке выше JavaScript-пакеты — это производственные сборки, то есть они оптимизированы за счет отладки. 21,1 КБ для пакета, специфичного для приложения, — это неплохо, но следует отметить, что никакой оптимизации кода (tree shaking) не происходит. Давайте посмотрим на код приложения и выясним, что можно сделать, чтобы это исправить.
В любом приложении поиск возможностей для оптимизации кода (tree shaking) будет заключаться в поиске статических операторов import . В верхней части файла основного компонента вы увидите строку примерно такого вида:
import * as utils from "../../utils/utils";
Импортировать модули ES6 можно различными способами , но вот этот заслуживает вашего внимания. В этой конкретной строке говорится: « import всё из модуля utils и поместите это в пространство имён с именем utils ». Главный вопрос здесь: «сколько всего содержится в этом модуле?»
Если вы посмотрите исходный код модуля utils , то увидите, что в нём около 1300 строк кода.
Вам всё это нужно ? Давайте перепроверим, поискав в основном файле компонента , который импортирует модуль utils , сколько раз встречается это пространство имён.

utils из которого мы импортировали множество модулей, вызывается в основном файле компонента всего три раза. Как оказалось, пространство имен utils встречается в нашем приложении всего в трех местах — но для каких функций? Если вы снова посмотрите на основной файл компонента, то увидите, что там всего одна функция — utils.simpleSort , которая используется для сортировки списка результатов поиска по ряду критериев при изменении значений в выпадающих списках сортировки:
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
Из файла, состоящего из 1300 строк и содержащего множество экспортируемых переменных, используется только одна. Это приводит к тому, что в итоге поставляется много неиспользуемого JavaScript-кода.
Хотя этот пример приложения, признаюсь, несколько надуман, это не меняет того факта, что подобный синтетический сценарий напоминает реальные возможности оптимизации, с которыми вы можете столкнуться в работающем веб-приложении. Теперь, когда вы определили возможность полезного применения «удаления лишних элементов» (tree shaking), как это делается на практике?
Предотвращение транспиляции модулей ES6 в модули CommonJS с помощью Babel
Babel — незаменимый инструмент, но он может несколько затруднить наблюдение за эффектами «удаления лишнего кода» (tree shaking). Если вы используете @babel/preset-env , Babel может преобразовывать модули ES6 в более совместимые модули CommonJS — то есть модули, которые вы подключаете с помощью ` require вместо import .
Поскольку оптимизация структуры файлов (tree shaking) сложнее для модулей CommonJS, webpack не будет знать, что нужно удалить из бандлов, если вы решите их использовать. Решение состоит в настройке @babel/preset-env таким образом, чтобы явно оставлять модули ES6 без изменений. Везде, где вы настраиваете Babel — будь то в babel.config.js или package.json — это предполагает добавление небольшого дополнительного параметра:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
Указание параметра modules: false в конфигурации @babel/preset-env заставляет Babel работать должным образом, что позволяет webpack анализировать дерево зависимостей и удалять неиспользуемые зависимости.
Учитывая побочные эффекты
Ещё один аспект, который следует учитывать при удалении зависимостей из вашего приложения, — это наличие побочных эффектов у модулей вашего проекта. Примером побочного эффекта является ситуация, когда функция изменяет что-то за пределами своей области видимости, что является побочным эффектом её выполнения:
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
В этом примере addFruit вызывает побочный эффект при изменении массива fruits , что выходит за рамки её области видимости.
Побочные эффекты также касаются модулей ES6, и это важно в контексте оптимизации кода (tree shaking). Модули, которые принимают предсказуемые входные данные и выдают столь же предсказуемые выходные данные, не изменяя ничего за пределами своей области видимости, являются зависимостями, от которых можно безопасно отказаться, если мы их не используем. Это самодостаточные, модульные фрагменты кода. Отсюда и название «модули».
Что касается webpack, то для указания того, что пакет и его зависимости не имеют побочных эффектов, можно использовать подсказку "sideEffects": false в файле package.json проекта:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
В качестве альтернативы вы можете указать webpack, какие именно файлы имеют побочные эффекты:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
В последнем примере любой файл, который не указан, будет считаться свободным от побочных эффектов. Если вы не хотите добавлять это в файл package.json , вы также можете указать этот флаг в конфигурации webpack через module.rules .
Импортируем только то, что необходимо.
После того, как мы указали Babel не трогать модули ES6, требуется небольшая корректировка синтаксиса import , чтобы подключить только необходимые функции из модуля utils . В примере этого руководства всё, что нужно, — это функция simpleSort :
import { simpleSort } from "../../utils/utils";
Поскольку импортируется только simpleSort , а не весь модуль utils , каждое упоминание utils.simpleSort необходимо заменить на simpleSort :
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
Этого должно быть достаточно для корректной работы команды tree shaking в этом примере. Вот вывод webpack до выполнения команды shaking для дерева зависимостей:
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
Вот результат после успешной очистки дерева от сорняков:
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
Хотя оба пакета уменьшились в размерах, наибольшую выгоду получил main пакет. Благодаря удалению неиспользуемых частей модуля utils , main пакет сокращается примерно на 60%. Это не только сокращает время загрузки скрипта, но и время его обработки.
Потрясите деревья!
Эффективность оптимизации кода (tree shaking) зависит от вашего приложения, его зависимостей и архитектуры. Попробуйте! Если вы точно знаете, что ваш сборщик модулей не настроен на эту оптимизацию, ничего страшного не случится, если вы попробуете и посмотрите, как это повлияет на ваше приложение.
Вы можете получить значительный прирост производительности от оптимизации кода (tree shaking), а можете и не заметить его вовсе. Но, настроив вашу систему сборки таким образом, чтобы она использовала эту оптимизацию в производственных сборках и выборочно импортировала только то, что необходимо вашему приложению, вы будете активно поддерживать минимальный размер пакетов вашего приложения.
Особая благодарность Кристоферу Бакстеру, Джейсону Миллеру , Адди Османи , Джеффу Поснику , Сэму Сакконе и Филипу Уолтону за их ценные замечания, которые значительно улучшили качество этой статьи.