TL;DR

  • Встряхнуть дерево не так просто, как кажется
  • Встряхивание дерева можно использовать только в том случае, если вы используете модули ES.
  • По умолчанию Babel не создает древовидные модули - сначала необходимо указать modules: false
  • Документация по настройке Webpack 4 представляет собой беспорядок, и нет очевидного способа проверить, какие части вашего пакета фактически обнаруживаются как неиспользуемые экспортные модули ES-модуля.
  • Если вы хотите создать древовидную библиотеку и опубликовать ее в npm, вы не можете сделать это с помощью webpack - вместо этого вам придется использовать rollup
  • Тот факт, что некоторые из экспорта ES6 обнаруживаются как неиспользуемые, не означает, что они будут автоматически удалены как мертвый код - некоторые вещи заставят minifier думать, что экспорт не может быть удален безопасно, потому что он имеет побочные эффекты.

Допустим, вы создаете интерфейсное приложение. На дворе 2018 год, поэтому вы используете наиболее часто используемые инструменты для своего приложения - React, webpack 4, babel для современных синтаксических функций ES и тому подобное.

Однажды вы замечаете, что ваша пачка довольно большая. Но есть это модное слово, которое вы слышали когда-то в связи с выпуском webpack 2. Оно обещает спасение, сокращение размеров пакетов и значительное ускорение работы вашего приложения.

Встряхивание деревьев.

Довольный своей хорошей памятью, вы начинаете гуглить, находить руководства, писать конфиги и без устали пытаться трясти эти деревья или что-то еще.

Но почему-то по неизвестным причинам все это просто не хочет работать так, как должно.

Дерево трясет 101

Давайте рассмотрим небольшое интерфейсное приложение:

Наш components.jsx файл экспортирует 2 простых компонента React - FooComponent и BarComponent. library.js файл экспортирует 2 функции - foo и bar.

Теперь предположим, что вам нужно создать пакет для этого простого приложения с main.js в качестве точки входа.

Встряхивание дерева - это процесс обнаружения и маркировки мертвого кода в вашем пакете на основе использования ES-модулями операторов import и export. В нашем примере только foo из library.js импортируется в main.js, поэтому мы ожидаем, что bar будет помечен как мертвый код, а затем удален минификатором, поддерживающим DCE (удаление мертвого кода). Точно так же в main.js импортируется только BarComponent, а не FooComponent, поэтому FooComponent следует автоматически опускать как мертвый код.

Конечно, алгоритмы удаления мертвого кода должны быть очень осторожны в отношении того, какой код можно и нельзя безопасно удалить. Общая идея заключается в том, что любой неиспользуемый код без побочных эффектов (т.е. изменение внешних переменных или глобального состояния, полифиллинг и тому подобное) можно безопасно удалить, неиспользуемый код с побочными эффектами должен быть сохранен, потому что мы не можем гарантировать, что эти побочные эффекты нежелательны или нежелательны, и в любых сомнительных ситуациях лучше перестраховаться и оставить код как есть.

Официальное руководство по веб-пакетам по этой теме обещает, что встряхивание дерева будет работать из коробки, и мы увидим unused harmony export комментариев в нашем пакете рядом с объектами, которые экспортируются, но нигде в приложении не используются. Посмотрим, как это работает для нас.

Встряхивание дерева с помощью webpack

Давайте создадим типичное приложение webpack.config.js для React. Нам нужно создать для него папку, сначала инициализировать package.json и npm install несколько зависимостей:

npm init --yes
npm install --save-dev react prop-types webpack webpack-cli babel-core babel-loader babel-preset-env babel-preset-react

Вот как будет выглядеть наш webpack.config.js:

В общем, довольно типичная конфигурация для любого приложения, использующего React. Обратите внимание, что мы помечаем все dependencies из нашего package.json - прямо сейчас это означает react и prop-types - как внешние, чтобы они были исключены из нашего набора и взяты из глобального. Мы делаем это только для того, чтобы упростить изучение пакета, а вы действительно не захотите делать это в реальном приложении.

Теперь мы можем его построить:

npx webpack --config webpack.config.js

Итак, рассмотрим комплект!

Су ... Кажется, деревья не трясутся. Никаких волшебных unused harmony export комментариев. Что только что пошло не так?

Уже видите?

No?

Позвольте мне дать вам подсказку - похоже, что при сборке нашего кода webpack вообще не подозревал, что эти файлы являются модулями ES. Они больше похожи на модули CommonJS - они используют exports.something для экспорта. Модули CommonJS не могут быть потрясены деревом, потому что их импорт и экспорт не статичны - вы можете добавить в module.exports все, что захотите, любым способом, которым вы сочтете нужным.

… Так лучше?

Видите ли, в webpack действительно входили обычные модули CommonJS. Это потому, что наши модули ES были фактически перенесены в CommonJS, т.е. _31 _ / _ 32_ были заменены на require и module.exports.

Встряхивание дерева с помощью webpack - первое, что может пойти не так

Если вы еще не догадались, виноват здесь Вавилон, а именно babel-preset-env. Одна из функций env preset - по умолчанию переносить любой тип модуля в модуль CommonJS.

Это не должно быть слишком сложно исправить - мы просто обновим наши babel-loader параметры и укажем пресет env, чтобы прекратить перенос модулей:

use: {
  loader: "babel-loader",
  options: {
    presets: [
      ["env", {modules: false}],
      "react"
    ]
  }
}

Итак, это первая проблема, с которой вы можете столкнуться.

Встряхивание дерева не работает с модулями CommonJS и работает только с модулями ES. Чтобы он работал с Babel, вы должны указать modules: false в своей babel-preset-env конфигурации.

Посмотрим, как сейчас пойдет. Я опускаю все скучные веб-пакеты из пакета и просто показываю вам, как выглядят наши модули:

Эти harmony import и harmony export комменты выглядят многообещающе, но гид обещал нам, что он также обнаружит отметки о неиспользуемых экспортах с unused harmony export комментарием, чего явно не делает прямо сейчас. Так что это не похоже на то, чтобы что-то поколебать.

Дрожание дерева с помощью webpack - второе, что может пойти не так

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

Одна важная вещь, о которой не упоминается в руководстве, заключается в том, что анализ использования импорта и экспорта ES отключен по умолчанию и включен только в том случае, если вы укажете mode: “production” в конфигурации вашего веб-пакета, но тогда, поскольку производственный режим также минимизируется по умолчанию, вы не увидите никаких комментариев вообще. Что еще более странно, на данный момент официальные документы веб-пакета не упоминают, что эта опция вообще существует - мне пришлось покопаться в источниках, чтобы узнать, что это возможно. Нам нужны настройки optimization.providedExports и optimization.usedExports, по умолчанию они отключены, и мы можем включить их, добавив к нашему webpack.config.js следующее:

optimization: {
  providedExports: true,
  usedExports: true
}

Это второе, что может пойти не так.

Чтобы веб-пакет проанализировал пакет на предмет неиспользуемых экспортов (и см. unused harmony export комментарии, как обещает руководство по встряхиванию дерева веб-пакетов), вам необходимо включить недокументированные флаги оптимизации - optimization.providedExports и optimization.usedExports в конфигурации веб-пакета.

Вот как выглядит модуль components.jsx в комплекте:

Ура! Теперь похоже, что дерево будет просто потрясено, не так ли?

Верно?

(спойлер: нет, совсем не в порядке)

Но мы вернемся к этому чуть позже.

А пока давайте просто подумаем, что все работает так, как мы ожидали, и рассмотрим другую ситуацию. Предположим, мы хотим извлечь наш components.jsx в отдельную библиотеку, которая будет использоваться, скажем, парой других подобных приложений. Конечно, мы хотели бы, чтобы эти приложения могли объединять только те компоненты, которые им действительно нужны, другими словами, мы хотим…

Создайте древовидную библиотеку с помощью webpack

Есть инструкция по созданию библиотеки с помощью webpack.

В нем говорится, что для создания нашего приложения как библиотеки нам нужно правильно указать externals, чтобы они не были объединены (что мы уже сделали для краткости в нашем примере), и указать output.library и output.libraryTarget для создания модуль, который можно использовать как библиотеку.

Как мы уже обсуждали, только модули ES могут быть встряхнуты, поэтому, чтобы наша библиотека была древовидной, нам нужно указать output.libraryTarget со значением

Подождите минутку.

var означает, что возвращаемое значение точки входа в нашу библиотеку будет присвоено одной переменной. assign означает, что то же возвращаемое значение будет использоваться для переназначения существующей переменной. commonjs, commonjs2, amd, umd… Все они хороши, но они не обеспечивают статический импорт и экспорт, и их нельзя поколебать.

Вот и все?

Итак, вот третье, что может заставить вас биться головой об стену.

Если вы хотите создать древовидную библиотеку, вы не можете связать ее с webpack.

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

В руководстве по созданию библиотеки Webpack упоминается, что вы можете указать точку входа в библиотеку в поле package.json module, и этот веб-пакет распознает это как модуль ES. Но что, если в нашей библиотеке мы решим использовать какой-нибудь плагин babel, который включает малоизвестную языковую функцию, которая все еще находится в статусе соломы? Фактически, даже React JSX уже сломал бы потребителей нашей будущей библиотеки.

Что нам нужно от библиотеки, так это иметь все, кроме _64 _ / _ 65_ операторов. И мы не можем сделать это с помощью webpack, потому что он еще не поддерживает эту конкретную целевую библиотеку.

Фактически - и мне пришлось некоторое время копать, чтобы выяснить это - рекомендуется использовать веб-пакет для приложений и накопительный пакет для библиотек.

Создайте древовидную библиотеку с помощью накопительного пакета

К счастью, rollup делает именно то, что нам нужно в нашем случае.

Давайте напишем простую сводную конфигурацию и попробуем объединить наш components.jsx в ES-модуль с возможностью встряхивания. Нам сначала нужно npm install --save-dev rollup rollup-plugin-babel. Наш конфиг будет выглядеть так:

Давайте объединим наши компоненты с npx rollup --config rollup.config.js и посмотрим, что мы получим в выходном пакете:

Это выглядит многообещающе!

Давайте переместим наш components.jsx в отдельную папку ../library, создадим для него package.json и переместим туда наш rollup.config.js. package.json для нашей библиотеки будет выглядеть примерно так:

Обратите внимание на поле module вместо main - оно должно указать webpack сначала попробовать использовать этот файл как модуль ES. Давайте построим его снова с npm install и npx rollup --config rollup.config.js. Наш пакет должен остаться неизменным, поэтому в нашей основной библиотеке теперь мы можем создать символическую ссылку для эмуляции установки пакета с npm install ../library. Давайте также изменим текст в нашем components.jsx на что-то вроде Hello from FooComponent living in separate library, {name}!, чтобы различать старые и новые компоненты.

Теперь давайте убедимся, что в нашей библиотеке можно встряхивать деревья. Давайте заменим код в нашем main.js следующим:

Нам также необходимо убедиться, что some-awesome-library действительно включен в пакет, поэтому он не должен быть помечен как внешний:

externals: Object.keys(dependencies).filter(package => package !== 'some-awesome-library')

Мы можем попробовать связать все это с npx webpack --config webpack.config.js и изучить наш пакет:

Отлично! Webpack правильно определил, что FooComponent не используется. Теперь все, что осталось, - это минимизировать вывод, чтобы увидеть, действительно ли неиспользуемые экспортные данные удалены из нашего кода. Теперь мы также можем удалить externals из нашего webpack.config.js и связать react вместе со всем остальным.

Мы можем сделать это, запустив webpack в производственном режиме:

npx webpack --config webpack.config.js --mode production

Это создает минифицированную сборку. Если мы попытаемся переформатировать выходной пакет с помощью prettier, мы сможем найти там следующие вызывающие беспокойство строки кода:

Итак, вот что происходит: мы создали древовидную библиотеку, построили ее без минификатора и увидели, что веб-пакет правильно пометил неиспользуемый компонент комментарием unused harmony export, и мы ожидали, что этот неиспользуемый экспорт будет быстро удален из нашего пакета как мертвый код. Но вот оно.

Дрожание дерева - третье, что может пойти не так

И это последнее и, вероятно, самое неприятное, что может пойти не так.

Вы видите, кто здесь виноват?

Я определенно не знал, когда впервые столкнулся с ней, и путем утомительных проб и ошибок мне удалось сузить круг и воспроизвести проблему с помощью следующего фрагмента кода:

(function() {
  var someDeadCodeFunction = function() {return 'dead code!';}
  someDeadCodeFunction.foo = 42;
})()

Когда вы запустите npx uglifyjs --compress --verbose -- input.js с указанным выше содержанием input.js, вы получите следующее:

WARN: Dropping side-effect-free statement [input.js:3,2]
WARN: Dropping unused variable someDeadCodeFunction [input.js:2,6]
WARN: Dropping side-effect-free statement [input.js:1,0]

Однако если вы добавите строку кода, чтобы ваш input.js выглядел так:

(function() {
  var someDeadCodeFunction = function() {return 'dead code!';}
  someDeadCodeFunction.foo = 42;
  someDeadCodeFunction.bar = 43;
})()

… Результат следующий:

!function(){var someDeadCodeFunction=function(){return"This is dead code!"};someDeadCodeFunction.foo=42,someDeadCodeFunction.bar=43}();

Итак, как вы догадались, виноват в этом UglifyJS.

Многие вещи, которые вы можете делать с export-ed переменными, функциями и классами (например, добавление к ним двух статических свойств), считаются побочными эффектами и предотвращают обнаружение этого конкретного export как мертвого кода UglifyJS, даже если webpack помечает их как неиспользуемые.

Итак, в нашем случае встряхивание дерева не сработало, потому что мы добавили свойства propTypes и defaultProps к нашим компонентам, что считается побочным эффектом. Причины этого не так очевидны, и мне не удалось найти никакой полезной информации по этому поводу. Я недостаточно разбираюсь в UglifyJS, чтобы делать здесь предположения, но если бы я мог, я бы подумал, что UglifyJS априори предполагает, что доступ к свойствам может означать вызов функции получения или установки, что может иметь побочные эффекты. Для этого есть даже compress вариант, но он по какой-то причине все равно не сработал.

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

Итак, как бы круто это ни звучало, встряхивание деревьев все еще довольно далеко от серебряной пули и работает так, как вы ожидаете. Здесь действуют несколько факторов, не в последнюю очередь из-за того, что документация webpack представляет собой полный беспорядок с момента выпуска версии 4.

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

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