Вопреки тому, что думают большинство разработчиков, дрожание деревьев не очень сложно. Обсуждение вокруг номенклатуры (устранение мертвого кода против дрожания деревьев) может ввести некоторую путаницу, но этот вопрос, наряду с некоторыми другими, проясняется на протяжении всей статьи. Как авторы библиотеки JavaScript, мы хотим достичь максимально легкого комплекта кода. В этом посте я проведу вас самыми популярными шаблонами, которые деоптизируют ваш код, а также поделюсь моим советом о том, как рассмотреть определенные случаи или проверить вашу библиотеку.
Немного теории
Встряхивание деревьев – это причудливый термин для устранения мертвого кода. Там нет точного определения этого. Мы можем рассматривать его как синоним устранения мертвого кода или попытаться поместить только определенные алгоритмы в этот зонт.
Если мы посмотрим на определение, указанное в Страница документов Webpack , кажется, упоминает оба подхода.
«Дерево встряхивание-это термин, обычно используемый в контексте JavaScript для устранения мертвого кода. Он опирается на статическую структуру синтаксиса модуля ES2015, то есть импорт и экспорт ».
Первое предложение подразумевает, что это синоним, в то время как второй упоминает некоторые конкретные языковые особенности, которые используются этим алгоритмом.
Номенклатура спора
«Вместо того, чтобы исключать мертвый код (устранение мертвого кода), мы включаем код живого кода (устранение дерева)» , различает Рича Харриса в его Отличный пост по теме Анкет
Одно практическое различие между обоими подходами заключается в том, что Так называемое дрожание деревьев обычно относится к работе, выполняемым бундлерами, тогда как устранение мертвого кода выполняется миниамиями, такими как Terser Анкет В результате весь процесс оптимизации окончательного вывода часто имеет 2 шага, если мы обсуждаем создание готовых к производству файлов. Фактически, WebPack активно избегает выборов мертвых кода и разгружает часть этой работы в Terser, бросая только необходимые биты. Все это – упростить работу для Terser, поскольку она работает на файлах и не знает о модулях или структуре проекта. Rollup, с другой стороны, делает вещи трудным путем и реализует больше эвристики в своей основе, что позволяет создавать меньше кода. Тем не менее, он по -прежнему рекомендуется запустить полученный код через Terser, чтобы добиться наилучшего общего эффекта.
Если вы спросите меня, есть мало смысла утверждать, какое определение верно. Это все равно, что бороться с тем, следует ли мы сказать параметры функций или аргументы функций. Существует разница в смысле, но люди так долго использовали термины, что эти термины стали взаимозаменяемыми в повседневном использовании. Говоря о дрожании деревьев, я понимаю точку зрения Рича, но я также думаю, что попытка отличить отдельные подходы внесли больше путаницы, чем разъяснения, и это в конечном итоге, оба метода проверяют одни и те же вещи. Вот почему я собираюсь использовать оба термина взаимозаменяемо на протяжении всего этого поста.
Зачем вообще беспокоиться?
Сообщество Frontend часто, кажется, одержимо размером с пакеты JavaScript, которые мы отправляем нашим клиентам. Существуют некоторые очень веские причины этой проблемы, и мы определенно должны обратить внимание на то, как мы пишем код, как мы структурируем наши приложения и какие зависимости мы включаем.
Основной мотивирующий фактор состоит в том, чтобы отправить меньше кода в браузер, что переводится как к более быстрой загрузке, так и в выполнении, что, в свою очередь, означает, что наши сайты могут отображаться или стать интерактивными быстрее.
Нет магии
В настоящее время популярные инструменты, такие как WebPack, Rollup, Terser и другие, не реализуют много чрезмерно сложных алгоритмов для отслеживания вещей с помощью границ функции/методов и т. Д. Делать это на таком очень динамичном языке, как JavaScript было бы чрезвычайно сложно. Такие инструменты, как Google Closure Compiler, гораздо более сложны, и они способны выполнять более продвинутый анализ, но они довольно непопулярны и, как правило, трудно настроить.
Учитывая, что в том, что делают эти инструменты, не так много волшебства, некоторые вещи просто не могут быть оптимизированы ими. Золотое правило состоит в том, что если вы заботитесь о размере пакета, вы должны предпочитать композиционные части, а не функции с тоннами опций или классов с большим количеством методов и так далее. Если ваша логика встраивает слишком много, и ваши пользователи используют только 10% от этого, они все равно будут оплачивать стоимость всего 100% – используя в настоящее время популярное инструменты, там просто невозможно.
Общее представление о том, как работают мини -и пучки
Любой данный инструмент, выполняющий анализ статического кода, работает на абстрактном синтаксисном представлении дерева вашего кода. Это в основном исходный текст программы, представленной объектами, которые образуют дерево. Перевод в значительной степени 1: 1, и преобразование между исходным текстом и AST семантически обратимо-вы всегда можете покинуть свой исходный код в AST, а затем сериализовать его обратно в семантически эквивалентный текст. Обратите внимание, что в JavaScript такие вещи, как пробелы или комментарии, не имеют семантического значения, и большинство инструментов не сохраняют ваше форматирование. Эти инструменты должны выяснить, как ведет себя ваша программа, не выполняя программу. Это включает в себя много книжных и перекрестных ссылок, выведенных на основе этой AST. Основываясь на этом, инструменты могут отбрасывать определенные узлы с дерева, как только они докажут, что это не повлияет на общую логику программы.
Побочные эффекты
Учитывая язык, который вы используете, определенные языковые конструкции лучше других для анализа статического кода. Если мы рассмотрим эту основную программу:
function add(a, b) {
return a + b
}
function multiply(a, b) {
return a * b
}
console.log(add(2, 2))
Мы можем с уверенностью сказать, что все Умножьте Функция не используется этой программой и, следовательно, не должна быть включена в окончательный код. Простое правило, чтобы помнить, это Функция почти всегда может быть безопасно удалена, если она остается неиспользованной, потому что простое объявление не выполняет никаких побочных эффектов Анкет
Побочные эффекты являются наиболее важной частью, чтобы понять здесь. Это то, что на самом деле влияет на внешний мир, например, призыв к Консоль.log является побочным эффектом, потому что он дает наблюдаемый результат программы. Было бы не нормально удалить такой вызов, как обычно ожидают его увидеть. Трудно перечислить все возможные типы побочных эффектов, которые программа может иметь, но и назвать несколько:
- Присвоение свойства мировому объекту, таким как
окно - Изменение всех других объектов
- Вызов многие встроенные функции, такие как
Fetch - Вызов пользовательских функций, которые содержат побочные эффекты
Код, который не имеет побочных эффектов, называется чистый Анкет
Минификаторы и бундлеры должны всегда принимать худшие и играть в безопасном Поскольку удаление любой заданной строки кода неправильно может быть очень дорогостоящим. Это может чрезвычайно изменить поведение программы и тратить время на то, чтобы отладки странными проблемами, которые проявляются только на производстве. (Министерство кода во время разработки не является популярным выбором.)
Популярные деоптимизирующие узоры и как их исправить
Как упоминалось в начале, эта статья посвящена в основном авторам библиотеки. Разработка приложений обычно фокусируется на функциональности, а не на оптимизации. Чрезмерная оптимизация аспектов, упомянутых ниже в коде приложения, как правило, не рекомендуется. Почему? Кодовая база приложений должна содержать только тот код, который фактически используется-прибыль, поступающая от реализации методов, вызванных бровями, была бы незначительной. Держите ваши приложения простыми и понятными.
💡 Действительно стоит отметить, что любой совет, данный в этой статье, действителен только для пути инициализации ваших модулей, для того, что выполняется сразу же, когда вы импортируете конкретный модуль. Код в функциях, классах и других в основном не является предметом этого анализа. Или, чтобы выразить это по -другому, такой код редко не используется и легко обнаружен, снимая правила, как без unced-vars и Непомощный Анкет
Доступ к недвижимости
Это может быть удивительно, но даже чтение собственности не может быть безопасно сброшено:
const test = someFunction() test.bar
Проблема в том, что бар Свойство на самом деле может быть функцией Getter, и функции всегда могут иметь побочные эффекты. Учитывая, что мы мало знаем о некоторая функция Поскольку его реализация может быть слишком сложной, чтобы ее анализировали, мы должны принять наихудший сценарий: это потенциальный побочный эффект и, как таковой, не может быть удален. То же правило применяется при назначении свойству.
Функциональные вызовы
Обратите внимание, что даже если бы мы смогли удалить эту операцию чтения свойств, мы все равно останетесь следующим образом:
someFunction()
Поскольку выполнение этой функции потенциально приводит к побочным эффектам.
Давайте рассмотрим несколько иной пример, который может напоминать какой-то реальный код:
export const test = someFunction()
Предположим, что благодаря алгоритмам дрожания дерева в связке, мы уже знаем, что тест не используется и, следовательно, может быть сброшен, что оставляет нас с:
const test = someFunction()
Простая операция объявления переменной также не содержит никаких побочных эффектов, поэтому его можно также отбросить:
someFunction()
Однако во многих ситуациях сам вызов не может быть отброшен.
Чистые аннотации
Есть ли что -нибудь, что можно сделать? Оказывается, решение довольно простое. Мы должны аннотировать звонок особым комментарием, который понимает мини -инструмент. Давайте сломем все это вместе:
export const test = /* #__PURE__ */ someFunction()
Эта маленькая вещь говорит нашим инструментам, что если результат аннотированной функции остается неиспользованным, то этот вызов может быть удален, что, в свою очередь, может привести к тому, что вся объявление функции будет отброшено, если ничто иное не относится к этому.
Фактически, части кода выполнения, сгенерированные бундлерами, также аннотируются такими комментариями, оставляя возможность сгенерированного кода, который будет отброшен позже.
Чистые аннотации против доступа к собственности
Делает /* #__Pure__ */ Работать на Getters и Setters? К сожалению нет. С ними мало что можно сделать без изменения самого кода. Лучшее, что вы могли бы сделать, это перенести их на функции. В зависимости от ситуации, может быть возможно реорганизовать следующий код:
const heavy = getFoo().heavy
export function test() {
return heavy.compute()
}
К этому:
export function test() {
let heavy = getFoo().heavy
return heavy.compute()
}
И если то же самое тяжелый экземпляр необходим для всех будущих вызовов, вы можете попробовать следующее:
let heavy
export function test() {
// lazy initialization
heavy = heavy || getFoo().heavy
return heavy.compute()
}
Вы можете даже попытаться использовать #__Pure__ С iife, но он выглядит чрезвычайно странно и может поднять брови:
const heavy = /* #__PURE__ */ (() => getFoo().heavy)()
export function test() {
return heavy.compute()
}
Соответствующие побочные эффекты
Безопасно ли аннотировать побочные функции, подобные этому? В контексте библиотеки это обычно. Даже если определенная функция имеет некоторые побочные эффекты (в конце концов, очень распространенный случай), они обычно имеют отношение только к тому, что результат такой функции остается использованным. Если код в функции не может быть безопасно отброшен без изменения поведения общей программы, вы обязательно не должны аннотировать такую функцию.
Встроенные
Что также может стать сюрпризом, так это то, что даже некоторые известные встроенные функции часто не признаются автоматически «чистыми».
Есть несколько веских причин для этого:
- Инструмент обработки не может знать, в какой среде ваш код действительно будет выполнен, так что, например,
Object.assign ({}, {foo: 'bar'})Вполне может просто бросить ошибку, например «Uncaught Typeerror: Object.Assign – это не функция». - Среда JavaScript может быть легко манипулирована некоторым другим кодом, о котором не знает инструмент обработки. Рассмотрим модуль мошенничества, который выполняет следующее:
Math.random () {бросить новую ошибку ('oops.')}Анкет
Как вы можете видеть, не всегда можно безопасно предполагать даже основное поведение.
Некоторые инструменты, такие как Rollup, решают быть немного более либеральными и выбирать прагматизм из -за гарантированной правильности. Они могут взять на себя не изменяемую среду и, по сути, позволяют получить более оптимальные результаты для наиболее распространенных сценариев.
Код сгенерированного транспортом
Довольно легко оптимизировать ваш код, как только вы посыпаете его #__Pure__ Аннотации, учитывая, что вы не используете никаких дополнительных инструментов переноса кода. Тем не менее, мы часто передаем наш код через такие инструменты, как Babel или TypeScript, чтобы создать окончательный код, который будет выполняться, и сгенерированный код нельзя легко контролировать.
К сожалению, некоторые основные преобразования могут деоптимизировать ваш код с точки зрения его пролетной, поэтому иногда проверка сгенерированного кода может быть полезна при поиске этих шаблонов деоптизации.
Я проиллюстрирую, что я имею в виду, с простым классом, имеющим статическое поле. ( Статические поля классов станут официальной частью языка с предстоящей спецификацией ES2021, но они уже широко используются разработчиками.)
class Foo {
static defaultProps = {}
}
Вывод вавилона:
class Foo {}
_defineProperty(Foo, "defaultProps", {});
Вывод типографии:
class Foo {}
Foo.defaultProps = {};
Используя знания, полученные на протяжении всей этой статьи, мы видим, что оба выхода были деоптизированы таким образом, чтобы другие инструменты могли бы правильно обработать. Оба выхода ставят статическое поле вне объявления класса и назначают выражение свойству – либо напрямую, либо через DefineProperty Вызовите (где последнее является более правильным в соответствии со спецификацией). Обычно такой сценарий не обрабатывается такими инструментами, как Terser.
Стороны: Неверно
Было быстро понято, что тряск дерева может автоматически принести только некоторые ограниченные преимущества большинству пользователей. Результаты сильно зависят от включенного кода, поскольку многие коды в дикой природе используют вышеупомянутые деоптимизирующие шаблоны. На самом деле, эти деоптимизирующие закономерности не являются по своей природе плохие, и большую часть времени не следует рассматривать как проблематичные; Это нормальный код.
Убедиться, что код не использует эти деоптимизирующие шаблоны, в настоящее время является в основном ручной работой, поэтому поддержание библиотечного дерева, как правило, является сложной задачей в долгосрочной перспективе. Довольно легко ввести безвредный нормальный код, который случайно начнет удерживать слишком много.
Следовательно, был введен новый способ аннотировать весь пакет (или просто конкретные файлы в пакете) в качестве побочных эффектов.
Можно положить «Стороны»: ложь в package.json вашего пакета, чтобы сообщить бундлерам, что файлы в этом пакете чисты в подобном смысле, который был описан ранее в контексте #__Pure__ аннотации.
Однако я считаю, что то, что он делает, в значительной степени неправильно понято. На самом деле это не работает как глобальный #__Pure__ Для вызовов функций в этом модуле, а также не влияет на Getters, Setters или что -либо еще в пакете. Это всего лишь часть информации для объединения, что, если ничего не использовалось из файла в таком пакете, то весь файл может быть удален, не изучая его содержание.
Чтобы проиллюстрировать эту концепцию, мы можем представить себе следующий модуль:
// foo.js
console.log('foo initialized!')
export function foo() {
console.log('foo called!')
}
// bar.js
console.log('bar initialized!')
export function bar() {
console.log('bar called!')
}
// index.js
import { foo } from './foo'
import { bar } from './bar'
export function first() {
foo()
}
export function second() {
bar()
}
Если мы только импортируем первый Из модуля, тогда Бундлер будет знать, что может опустить целое ./bar.js Файл (благодаря "sideefcects": false flag). Итак, в конце концов, это будет зарегистрировано:
foo initialized! foo called!
Это довольно улучшение, но в то же время, по моему скромному мнению, это не серебряная пуля. Основная проблема с этим подходом заключается в том, что нужно быть очень осторожным с тем, как код организован внутри (структура файла и т. Д.) Для достижения наилучших результатов. В прошлом это был обычный совет по «плоскому связующему» коде, но в этом случае он наоборот – Плоское объединение активно вредно для этого флага Анкет
Это также может быть легко деоптизизировано, если мы решим использовать что -нибудь еще из ./bar.js Файл, потому что он будет отброшен только если Нет экспорта Из модуля в конечном итоге используется.
Как проверить это
Тестирование сложно, особенно потому, что разные инструменты дают разные результаты. Есть несколько хороших пакетов, которые могут вам помочь, но я обычно считал их ошибочными, так или иначе.
Обычно я пытаюсь вручную проверить пакеты, которые я получаю после запуска WebPack & Rollup на файле, как это:
import 'some-library'
Идеальный результат – пустой пакет – в нем нет кода. Это редко случается, поэтому требуется ручное расследование. Можно проверить, что попало в пакет, и выяснить, почему это могло произойти, зная, что вещи могут определить такие инструменты.
С присутствием «Стороны»: ложь , мой подход может легко привести к ложноположительным результатам. Как вы могли заметить, приведенный выше импорт не использует экспорт Некоторая библиотека Таким образом, это сигнал для бундлера, что вся библиотека может быть отброшена. Это не отражает то, как все используется в реальном мире.
В таком случае я пытаюсь проверить библиотеку после удаления этого флага из его Package.json Чтобы проверить, что произойдет без этого, и посмотреть, есть ли способ улучшить ситуацию.
Счастливого дерева встряхивая!
Не забудьте проверить наш Другой контент на dev.to! Если вы хотите сотрудничать с нами в расширении области деловых сообщений, посетите наш Программа разработчиков !
Оригинал: “https://dev.to/livechat/tree-shaking-for-javascript-library-authors-4lb0”