Девятая серия принципов Webpack: принцип реализации Tree-Shaking

внешний интерфейс JavaScript
Девятая серия принципов Webpack: принцип реализации Tree-Shaking

1. Что такое встряска дерева

Tree-Shaking - это технология устранения мертвого кода, основанная на спецификации модуля ES. Он экспортируется между модулем холодильника во время прогона. Определите, какое значение экспорта в модуле ESM не использовалось, и удаляет его, улучшить оптимизацию упакованных товар.

Tree Shaking был впервые реализован Ричем Харрисом в Rollup ранее, Webpack был интегрирован с версии 2.0 и стал широко используемым методом оптимизации производительности.

1.1 Запустите встряхивание дерева в Webpack

В Webpack для запуска функции встряхивания дерева должны одновременно выполняться три условия:

  • Напишите код модуля, используя спецификацию ESM.
  • настроитьoptimization.usedExportsдляtrue, чтобы запустить функцию маркировки
  • Запустите функцию оптимизации кода, что может быть достигнуто следующими способами:
    • настроитьmode = production
    • настроитьoptimization.minimize = true
    • поставкаoptimization.minimizerмножество

Например:

// webpack.config.js
module.exports = {
  entry: "./src/index",
  mode: "production",
  devtool: false,
  optimization: {
    usedExports: true,
  },
};

1.2 Теоретическая основа

В более старых модульных схемах JavaScript, таких как CommonJs, AMD, CMD и т. д., поведение импорта и экспорта очень динамично и непредсказуемо, например:

if(process.env.NODE_ENV === 'development'){
  require('./bar');
  exports.foo = 'foo';
}

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

if(process.env.NODE_ENV === 'development'){
  import bar from 'bar';
  export const foo = 'foo';
}

Следовательно, зависимости между модулями в ESM сильно детерминированы и не имеют ничего общего с рабочим состоянием.Инструменту компиляции нужно только выполнить статический анализ модуля ESM, и он может сделать вывод, какие значения модуля не использовались другими модулями. из литералов кода, что является необходимым условием для реализации технологии Tree Shaking.

1.3 Пример

Для следующего кода:

// index.js
import {bar} from './bar';
console.log(bar);

// bar.js
export const bar = 'bar';
export const foo = 'foo';

В примереbar.jsмодуль экспортируетсяbar,foo, но толькоbarЭкспортированное значение используется другими модулями после обработки Tree Shake,fooПеременные удаляются как бесполезный код.

Во-вторых, принцип реализации

В Webpack реализация Tree-shaking стоит на первом месте.отметкаКакие из значений экспорта модуля не используются, второе — использовать Terser для удаления этих неиспользуемых операторов экспорта. Процесс маркировки можно условно разделить на три этапа:

  • На этапе создания переменные экспорта модуля собираются и записываются в переменную ModuleGraph графа зависимостей модуля.
  • На этапе уплотнения просмотрите ModuleGraph, чтобы отметить, используются ли экспортированные переменные модуля.
  • При создании продукта, если переменная не используется другими модулями, удалите соответствующий оператор экспорта.

Функция тега должна быть настроенаoptimization.usedExports = trueвключи

То есть действие тега заключается в удалении операторов экспорта, которые не используются другими модулями, например:

В примереbar.jsМодуль (второй слева) экспортирует две переменные:barа такжеfoofooОн не используется другими модулями, поэтому после маркировки продукт сборки (первый справа)fooОператор экспорта, соответствующий переменной, удаляется. Для сравнения, если функция тегирования не включена (optimization.usedExports = falseВремя), переменная используется с или без экспорта или без экспорта, как показано на фиг.

Обратите внимание, что на этот разfooКод, соответствующий переменнойconst foo='foo'все еще не повреждены, это потому, что функция маркировки влияет только на оператор экспорта модуля, который фактически выполняется"Shaking” работает с подключаемым модулем Terser. Например, в приведенном выше примереfooПосле того, как переменная помечена, она стала мертвым кодом - кодом, который не может быть выполнен.В настоящее время вам нужно только использовать функцию DCE, предоставленную Terser, чтобы удалить этот раздел оператора определения, чтобы добиться полного эффекта Tree Shaking.

Далее я раскрою исходный код процесса тегирования и подробно объясню процесс реализации Tree Shaking в Webpack 5. Учащиеся, которым не интересен исходный код, могут перейти к следующей главе.

2.1 Собрать экспорт модулей

Прежде всего, Webpack необходимо выяснить, какое значение экспорта имеет каждый модуль.Этот процесс происходит на этапе создания.Общий процесс выглядит следующим образом:

Дополнительные инструкции по этапу Make см. в предыдущем разделе.[Обзор из 10 000 слов] Узнайте об основных принципах Webpack в одной статье.

  1. Преобразуйте все операторы экспорта ESM модуля в объекты зависимостей и запишите их вmoduleобъектdependenciesСбор, правила трансформации:
  • Именованный экспорт преобразуется вHarmonyExportSpecifierDependencyобъект
  • defaultэкспортировать конвертировать вHarmonyExportExpressionDependencyобъект

Например, для следующего модуля:

export const bar = 'bar';
export const foo = 'foo';

export default 'foo-bar'

соответствующийdependenciesЗначение:

  1. После компиляции всех модулей запускаемcompilation.hooks.finishModulesхук, начать выполнениеFlagDependencyExportsPluginОбратный вызов плагина
  2. FlagDependencyExportsPluginПлагин считывает информацию о модуле, хранящуюся в ModuleGraph, из записи и проходит через всеmoduleобъект
  3. траверсmoduleобъектdependenciesмассив, найти всеHarmonyExportXXXDependencyзависимый объект типа, преобразовать его вExportInfoобъект и записать его в систему ModuleGraph

проходить черезFlagDependencyExportsPluginПосле обработки плагина все операторы экспорта в стиле ESM будут записаны в систему ModuleGraph, а последующие операции могут напрямую считывать значения экспорта модуля из ModuleGraph.

Использованная литература:

  1. [Обзор из 10 000 слов] Узнайте об основных принципах Webpack в одной статье

  2. Немного сложное знание веб-пакета: глубокий анализ Dependency Graph

2.2 Маркировка экспорта модуля

После того, как информация об экспорте модуля собрана, WebPack необходимо отметить список экспорта каждого модуля, какое значение экспорта используется другими модулями, а что нет, этот процесс происходит на этапе SEAL, основной процесс:

  1. вызыватьcompilation.hooks.optimizeDependenciesхук, начать выполнениеFlagDependencyUsagePluginЛогика плагина
  2. существуетFlagDependencyUsagePluginВ плагине пройдитесь по всем модулям, хранящимся в ModuleGraph, начиная с записиmoduleобъект
  3. траверсmoduleобъект, соответствующийexportInfoмножество
  4. для каждогоexportInfoвыполнение объектаcompilation.getDependencyReferencedExportsметод определения соответствующегоdependencyИспользуется ли объект другими модулями
  5. Экспортируемые значения, используемые любым модулем, вызовexportInfo.setUsedConditionallyспособ пометить его как использованный.
  6. exportInfo.setUsedConditionallyВнутренняя модификацияexportInfo._usedInRuntimeсвойство, которое записывает, как используется экспорт
  7. конец

Вышеупомянутая версия является чрезвычайно упрощенной версией, и в середине по-прежнему много логики ветвления и сложных операций сбора.FlagDependencyUsagePluginВ подключаемом модуле результат выполнения в итоге будет записан в соответствующем операторе экспорта модуля.exportInfo._usedInRuntimeСловарь.

2.3 Генерация кода

После предыдущих шагов сбора и маркировки Webpack четко зафиксировал, какие значения экспортируются каждым модулем в системе ModuleGraph, и каждое экспортированное значение не используется этим модулем. Затем Webpack сгенерирует другой код на основе использования экспортированного значения, например:

Фокусbar.jsфайл, который также является значением экспорта,barодеялоindex.jsМодуль использует соответственно сгенерированные__webpack_require__.dпередача"bar": ()=>(/* binding */ bar), Для сравненияfooТогда сохраняется только оператор определения, а соответствующий экспорт в чанке не генерируется.

О содержании продуктов Webpack и__webpack_require__.dСмысл метода см.Принципы Webpack, серия 6: Тщательное понимание среды выполнения Webpackодна статья.

Логика генерации этого раздела соответствует оператору экспорта.HarmonyExportXXXDependencyРеализация класса, общий процесс:

  1. Этап упаковки, звонитеHarmonyExportXXXDependency.Template.applyспособ генерации кода
  2. существуетapplyВ методе прочитайте данные, хранящиеся в ModuleGraph.exportsInfoинформацию, чтобы определить, какие производные значения используются, а какие нет.
  3. Для экспортированных значений, которые использовались и которые не использовались, создайте соответствующиеHarmonyExportInitFragmentобъект, сохранить вinitFragmentsмножество
  4. траверсinitFragmentsмассив, производящий окончательный результат

По сути, логика этого шага заключается в использовании ранее собранногоexportsInfoЭкспортированные значения объектов и модулей генерируют отчеты об экспорте отдельно.

2.4 Удалить Dead Code

Через несколько шагов перед тем, как модуль не используется для получения списка значений, они не определены.__webpack_exports__В объекте формируется эффект Dead Code, который не может быть выполнен, как в приведенном выше примере.fooПеременная:

После этого инструменты DCE, такие как Terser и UglifyJS, «встряхнут» эту часть недопустимого кода, чтобы сформировать полную операцию встряхивания дерева.

2.5 Резюме

Таким образом, реализация Tree Shaking в Webpack разделена на следующие этапы:

  • существуетFlagDependencyExportsPluginПо данным модуляdependenciesСписок собирает значения экспорта модуля и записывает их в систему ModuleGraphexportsInfoсередина
  • существуетFlagDependencyUsagePluginИспользование экспортированного значения модуля собирается в подключаемом модуле и записывается вexportInfo._usedInRuntimeв коллекции
  • существуетHarmonyExportXXXDependency.Template.applyВ методе генерируются разные операторы экспорта в соответствии с использованием экспортируемого значения.
  • Удалите Dead Code с помощью инструмента DCE для полного эффекта дрожания дерева

Вышеупомянутый принцип реализации требует высоких базовых знаний.Рекомендуется, чтобы читатели сотрудничали со следующими документами, чтобы поесть:

  1. [Обзор из 10 000 слов] Узнайте об основных принципах Webpack в одной статье

  2. Немного сложное знание веб-пакета: глубокий анализ Dependency Graph

  3. Принципы Webpack, серия 6: Тщательное понимание среды выполнения Webpack

3. Лучшие практики

Хотя Webpack изначально поддерживает функцию Tree Shaking начиная с версии 2.x, она ограничена динамическими характеристиками JS и сложностью модулей.До последней версии 5.0 многие проблемы, вызванные побочными эффектами кода, не были решены, что делает Эффект оптимизации не так хорош, как Tree.Встряхивание изначально задумывалось настолько совершенным, поэтому пользователям необходимо сознательно оптимизировать структуру кода или использовать некоторые методы исправления, чтобы помочь Webpack более точно обнаруживать недопустимый код и завершать операцию встряхивания дерева.

3.1 Избегайте бессмысленных заданий

При использовании Webpack необходимо сознательно избегать некоторых ненужных операций присваивания.Обратите внимание на следующий пример кода:

В примереindex.jsссылки на модулиbar.jsмодульныйfooи назначитьfпеременная, но она не используется в последующихfooилиfпеременная, в данном случаеbar.jsмодуль экспортированfooЗначение на самом деле не используется и должно быть удалено, но операция Tree Shake Webpack не вступает в силу и остается в продукте.fooЭкспорт:

Неглубокая причина такого результата заключается в том, что логика Webpack Tree Shaking остается на уровне статического анализа кода и делает только поверхностное суждение:

  • Ссылаются ли на экспортируемую модулем переменную другие модули
  • Появляется ли эта переменная в основном коде модуля, на который ссылаются?

Без лишних слов проанализируйте семантически, действительно ли экспортируемое значение модуля используется эффективно.

Более глубокая причина заключается в том, что операторы присваивания в JavaScript нечистый, в зависимости от конкретного сценария могут возникнуть неожиданные побочные эффекты, такие как:

import { bar, foo } from "./bar";

let count = 0;

const mock = {}

Object.defineProperty(mock, 'f', {
    set(v) {
        mock._f = v;
        count += 1;
    }
})

mock.f = foo;

console.log(count);

В примере даmockналоженный объектObject.definePropertyвызов, в результате чегоmock.f = fooпара операторов присваиванияcountПеременные имеют побочные эффекты.В этом сценарии, даже используя сложный динамический семантический анализ, трудно идеально встряхнуть все бесполезные ветки кода, не обеспечив правильных побочных эффектов.

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

3.3 Использование#pureАннотирование вызовов чистых функций

Подобно операторам присваивания, операторы вызова функций в JavaScript также могут иметь побочные эффекты, поэтому по умолчанию Webpack не обрабатывает вызовы функций встряхивания дерева. Однако разработчики могут добавить/*#__PURE__*/Обратите внимание, явно сообщите Webpack, что этот вызов функции не будет иметь побочных эффектов в контексте, например:

В примереfoo('be retained')звонок не принес/*#__PURE__*/Примечания, код сохранен, для сравненияfoo('be removed')С заявлением Pure оно было удалено Tree Shaking.

3.3 Запретить импорт и экспорт модулей перевода Babel

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

Однако некоторые функции, предоставляемые Babel, сделают функцию Tree Shake недействительной.Например, Babel может использоватьimport/exportОператоры Style ESM эквивалентно переводятся в модульные операторы стиля CommonJS, но эта функция не позволяет Webpack выполнять статический анализ содержимого импорта и экспорта переведенного модуля.

Примерbabel-loaderиметь дело с*.jsфайл и установить элементы конфигурации Babelmodules = 'commonjs', перенос схемы модульности из ESM в CommonJS, в результате чего транспилированный код (вверху справа) неправильно помечает неиспользуемые значения экспортаfoo. Для сравнения на рисунке 2 справаmodules = falseРезультат упаковки, когдаfooПеременные правильно помечены как Dead Code.

Итак, в Webpack используйтеbabel-loader, рекомендуетсяbabel-preset-envизmoduelsЭлемент конфигурации настроен наfalse, чтобы отключить перевод операторов импорта и экспорта модуля.

3.4 Оптимизация детализации экспортируемых значений

Логика Tree Shake действует на ESMexportоператор, поэтому для следующего сценария экспорта:

export default {
    bar: 'bar',
    foo: 'foo'
}

даже если на самом деле толькоdefaultОдно из свойств производного значения, весьdefaultОбъект все равно останется нетронутым. Поэтому при реальной разработке следует максимально сохранить гранулярность и атомарность экспортируемых значений.Оптимизированная версия приведенного выше примера кода:

const bar = 'bar'
const foo = 'foo'

export {
    bar,
    foo
}

3.5 Использование пакетов, поддерживающих Tree Shaking

Если возможно, попробуйте использовать пакеты npm, поддерживающие Tree Shaking, например:

  • использоватьlodash-esзаменятьlodashили используйтеbabel-plugin-lodashдостичь аналогичного эффекта

Однако не во всех пакетах npm есть место для Tree Shaking. Такие фреймворки, как React и Vue2, уже максимально оптимизировали производственную версию. Слишком много нужно для встряхивания деревьев.

PS:

Команда разработчиков Byte Game активно набирает сотрудников в Шэньчжэне и Шанхае, JD:

默认标题_动态横版二维码_2021-08-24-0 (2).gif