Часть вторая: минификация и удаление мертвого кода

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

Во второй части нашей серии мы подробно рассмотрим минификацию и удаление мертвого кода. Мы выделим следующие темы:

  • История минификации и DCE
  • Что такое минификация
  • Что такое удаление мертвого кода
  • Как мы можем использовать инструмент для запуска этих процессов в нашем коде?
  • Сделайте это автоматизированным!

Если вы пропустили часть первую, не стесняйтесь сначала прочитать ее, чтобы узнать о комплектации.

Давайте начнем!

Минификация, история

Во-первых, давайте заглянем в историю.

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

По мере того, как использование JavaScript увеличивалось с помощью таких инструментов, как Moo-Tools и JQuery, были разработаны инструменты минификации, чтобы уменьшить следы CSS и JavaScript и, следовательно, уменьшить время загрузки начального запроса. Кроме того, мы переместили бы большие, часто используемые инструменты JavaScript, такие как JQuery, на CDN для более быстрой доставки контента. Рабочие сайты будут указывать на уменьшенные версии этих библиотек.

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

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

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

Примечание. Все примеры кода, использованные в этой части, можно найти на Github.

Предположим, что selectClothes выполняется из другого модуля в нашем приложении. В нашем примере невозможно получить объект Hat в качестве возвращаемого значения. Возможно, он был включен в какой-то момент, возможно, это был рефакторинг, который случайно оставил этот код, или кто-то решил, что пользователи не могут покупать шляпы. То, что осталось, называется «мертвым кодом». Несмотря на то, что этот код минимален, эти кусочки, безусловно, могут складываться, и они увеличивают размер нашего пакета. Как мы знаем, увеличенные размеры пакетов увеличивают время загрузки и требуют большего объема данных по сети.

Многие инструменты минификации, такие как Terser, уже включают устранение мертвого кода. Он обнаружит, что любой код, который никогда не выполняется, и автоматически удалит его из базы кода. Давайте попробуем передать наш пример кода через Terser, чтобы лучше понять саму библиотеку.

Примечание. Terser описывает себя как «набор инструментов для синтаксического анализа и управления / сжатия JavaScript для ES6 +».

Сначала создайте папку для этого проекта:

mkdir dce-example
cd dce-example

Добавьте приведенный выше код selectClothes в dce-example/index.js.

Наконец, давайте создадим пакет npm:

npm init

После инициализации мы можем добавить Terser:

npm i terser — save-dev

Теперь давайте сделаем первый шаг с Terser, запустим:

npx terser index.js

Результат должен выглядеть примерно так:

function selectClothes(type){if(type===”shirt”){return{type:”shirt”,amount:”$10"}}else{return{type:”pants”,amount:”$12"}}return{type:”hat”,amount:”$5"}}selectClothes(“shirt”);

Наш код уменьшен, но мертвый код не был удален из пакета. В чем дело? Мы скоро вернемся к этому, но сначала давайте рассмотрим миниатюрный код. Заметили, что все пробелы были удалены? Удаление этого места уменьшает размер файла. Но как насчет имен переменных / функций, они по-прежнему являются обычными именами, разве мы не говорили, что их следует уменьшить до минимально возможного размера?

Terser-CLI знает, что мы, вероятно, используем это и просматриваем в нашем терминале. Когда переменные и функции «искажены», очень сложно читать и понимать наш код, поэтому по умолчанию это не делается. Большинство веб-приложений используют настоящую систему пакетов модулей, чтобы делать это автоматически, поэтому им никогда не придется читать этот искаженный код. Однако ради обучения мы должны получить представление о том, как это выглядит, поэтому на этот раз давайте добавим еще пару команд в Terser:

npx terser — toplevel — mangle — mangle-props — index.js

Это даст нам наш искаженный код:

function t(t){if(t===”shirt”){return{type:”shirt”,t:”$10"}}else{return{type:”pants”,t:”$12"}}return{type:”hat”,t:”$5"}}t(“shirt”);

Устранение мертвого кода

Хорошо, давайте вернемся к нашему вопросу. Почему не был удален наш мертвый код? Что ж, опция Terser по умолчанию не включает сжатие, в котором происходит удаление мертвого кода. Итак, давайте добавим его, задав ему команду -c.

npx terser index.js -c

И наш результат должен выглядеть примерно так:

function selectClothes(type){return”shirt”===type?{type:”shirt”,amount:”$10"}:{type:”pants”,amount:”$12"}}selectClothes(“shirt”);

Посмотрите, насколько он меньше! Он не только удалил мертвый код (заметка hat полностью исчез), но также оптимизировал наш модуль, изменив наш if поток на троичный!

Автоматизация с помощью Webpack!

В какой-то момент нам действительно пришлось передать наш код через такие инструменты, как Ternary, прежде чем выпускать наше приложение в Интернет. Но с тех пор инструменты связывания, такие как Rollup и Webpack, захватили фронтенд-сообщество. Эти инструменты связывания обычно легко расширить с помощью таких плагинов, как Terser. В нашем примере мы собираемся использовать Webpack и добавить плагин Terser.

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

Итак, давайте добавим в наш проект Webpack и Webpack-CLI:

npm i webpack webpack-cli — save-dev

Мы будем добавлять и перемещать сюда пару файлов. Сначала мы добавим папки с именами dist и src:

mkdir dist
mkdir src

Внутри dist мы добавим файл index.html:

 <!doctype html>
 <html>
   <head>
    <title>Getting Started</title>
  </head>
  <body>
    <script src=”main.js”></script>
   </body>
 </html>

Затем мы перейдем к нашей папке /src в корне нашего проекта и переместим в нее index.js:

mv index.js src/

Наконец, нам нужно зайти в наш package.json и удалить свойство main, а вместо этого добавить private: true:

Наш проект настроен, поэтому мы вернемся в корень проекта и запустим инструмент webpack-CLI.

npx webpack

После этого мы можем открыть /dist/main.js, чтобы увидеть наш новый связанный файл JavaScript. Помните, что это добавление системы связывания в наше приложение, поэтому к файлу будет прикреплено довольно много шаблонов. Это значительный перебор из-за наличия только одного файла JavaScript. Но по мере того, как наше веб-приложение становится все более обширным, этот шаблон становится незначительным объемом кода.

Итак, давайте посмотрим на этот комплект!

!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){“undefined”!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:”Module”}),Object.defineProperty(e,”__esModule”,{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&”object”==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,”default”,{enumerable:!0,value:e}),2&t&&”string”!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,”a”,t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p=””,r(r.s=0)}([function(e,t){}]);

У вас может быть пара вопросов. Во-первых, мы говорили о добавлении плагина Terser, но он уже уменьшен и сжат. Дело в том, что Webpack автоматически добавляет плагин Terser для минификации. Кроме того, вы могли заметить предупреждение в своей консоли, когда мы запускали Webpack. Поскольку мы не указываем среду, предполагалось производство. Когда среда настроена на производство, она выполнит за нас минификацию и сжатие.

Может быть, еще более важный вопрос, где код, который мы написали? Что ж, самая последняя версия Webpack оборачивает ваш файл с помощью esmodules, поэтому он может выполнять более статический анализ вашего модуля. Он определил, что написанный нами код не повлиял на нашу кодовую базу. Функция никогда не вызывалась извне (подробнее об этом в третьей части) и никогда не выполняла никаких нечистых путей. Следовательно, весь файл был мертвым кодом. И это правильно! Мы никогда не использовали этот код с фактическим интерфейсом. Самый простой способ обойти это - добавить журнал консоли. Так что давайте сделаем это в нашем index.js.

Теперь, когда мы снова запускаем наш интерфейс командной строки webpack:

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){“undefined”!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:”Module”}),Object.defineProperty(e,”__esModule”,{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&”object”==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,”default”,{enumerable:!0,value:e}),2&t&&”string”!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,”a”,t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=””,n(n.s=0)}([function(e,t){const n=”shirt”===”shirt”?{type:”shirt”,amount:”$10"}:{type:”pants”,amount:”$12"};console.log(n)}]);

Теперь наш код находится в основном пакете. Также обратите внимание: как и раньше, наш желаемый мертвый код был удален! Давайте попробуем немного абстрагироваться от этого. В нашем index.js давайте обновим наш код, чтобы:

А теперь результат:

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){“undefined”!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:”Module”}),Object.defineProperty(e,”__esModule”,{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&”object”==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,”default”,{enumerable:!0,value:e}),2&t&&”string”!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,”a”,t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=””,n(n.s=0)}([function(e,t){const n=()=>({type:”pants”,amount:”$12"});const r=”shirt”===”shirt”?(()=>({type:”shirt”,amount:”$10"}))():n();console.log(r)}]);

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

const buyShirt=()=>({type:”shirt”,amount:”$10"}),buyPants=()=>({type:”pants”,amount:”$12"}),buyHat=()=>({type:”hat”,amount:”$5"});function selectClothes(type){return”shirt”===type?buyShirt():buyPants()}const clothes=selectClothes(“shirt”);console.log(clothes);

Итак, Webpack удалил неиспользуемую функцию, а Terser не может этого сделать, почему?

Это потому, что Webpack может предполагать, что мы используем систему esmodule. Начиная с Webpack 4, это используемая модульная система по умолчанию. Поскольку функция buyHat не exported (подробнее об этом позже), Webpack знает, что единственное место, где она используется в коде, - это наша функция selectClothes. Как только Terser удалит оттуда мертвый код, Webpack знает, что он больше не используется в кодовой базе, и его можно безопасно удалить. Это ранняя демка Treeshaking.

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

Заключительные мысли

Отличная работа! Мы прошли первые шаги по устранению мертвого кода. Если вы впервые столкнулись с чем-то вроде удаления мертвого кода и у вас возникли некоторые проблемы с его пониманием, у меня есть для вас несколько ссылок ниже. Не волнуйтесь, что в первый раз это может быть немного сложно, изучение того, как сам код JavaScript анализируется и компилируется, может быть пугающим. Это немного больше мета, чем мы обычно думаем о нашей системе.

С другой стороны, как только вы почувствуете это, на самом деле это довольно простой процесс! И при этом критический процесс. Мы собираемся перейти к Tree shaking в нашей следующей части, которая в значительной степени полагается на этот процесс как на строительный блок, поэтому я рекомендую вам немного поэкспериментировать с Terser и проверить дальнейшее чтение для получения более подробной информации. Так что присоединяйтесь к нам в следующий раз, чтобы узнать о современном встряхивании дерева JavaScript!

Если есть что-то, что вам интересно увидеть в этом блоге, или вы думаете, что мне стоит проверить, обязательно свяжитесь со мной @gitinbit. Весь код здесь доступен на Github. Ваше здоровье!

Ссылки и дополнительная литература

Https://dennyscott.io/reduce-js-bundle-part-one

Https://mootools.net/

Https://en.wikipedia.org/wiki/AngularJS

Https://jquery.com/

Https://www.cloudflare.com/learning/cdn/what-is-a-cdn/

Https://en.wikipedia.org/wiki/Single-page_application

Https://www.keycdn.com/support/the-growth-of-web-page-size

Https://en.wikipedia.org/wiki/Dead_code_elimination

Https://github.com/terser-js/terser

Https://github.com/rollup/rollup

Https://webpack.js.org/

Https://webpack.js.org/guides/tree-shaking/

Https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/

Https://developers.google.com/web/fundamentals/performance/optimizing-javascript/tree-shaking/