Оптимизация фронтенда. Часть 1. Почему я не люблю слово treeshaking или где вас обманывает webpack

в 8:49, , рубрики: javascript, treeshaking, webpack, Блог компании Wrike, Клиентская оптимизация, Разработка веб-сайтов

Оптимизация фронтенда. Часть 1. Почему я не люблю слово treeshaking или где вас обманывает webpack - 1
Мы относимся к технологиям, которые используем, как к покупкам на Яндекс маркете. Смотрим на спецификацию, читаем отзывы и, если проект получил много звездочек на гитхабе, проходит по спецификации, и к тому же внедрение стоит недорого, мы его  покупаем устанавливаем. Такой подход иногда очень сильно бьет по голове ручкой от граблей, и тогда все-таки приходится разбираться, что происходит.

Предыстория

В статье одного из авторов rollup рассмотрены две оптимизации, одна называется dead code elimination, а вторая tree-shaking.  Автор показывает, что у tree-shaking намного больше возможностей по сжатию кода.  И в доказательство приводит несколько соображений о рецептах пирога и разбившихся яйцах. Ох уж эти метафоры!

Эту идею (про tree-shaking, не про пирог и яйца) подхватила команда разработчиков webpack и с версии 2.0 стала официально поддерживать.

Проблема

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

Некоторые, конечно, догадываются о подвохе и даже пишут статьи на Хабр. Но любителей порассуждать о преимуществах tree-shaking над dead code illumination в webpack вокруг меньше не становится, по крайней мере, среди посетителей конференций и среди моих коллег.

Как это должно было работать?

Идея проста, как танк.

  1. Сборщик проходит по дереву модулей и помечает неиспользуемые импорты специальными комментариями. Вот так:
    ...
    /* unused harmony export square */
    function square(x) { return x * x;}
    ...
  2. Следующим этапом UglifyJS, который по умолчанию изолентой примотан к webpack, помимо всего того, что мы от него ждем, нещадно выпиливает код, который помечен этими самыми комментариями.
  3. PROFIT!

Когда это не работает?

Допустим, у нас все как в документации. Два файла index.js и module.js

// index.js

import {cube} from './module'

console.log(cube(x))

// module.js

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

Если мы сейчас запустим webpack в режиме оптимизации и минимизации, то все заработает как и ожидалось. (код)

webpack --optimize-minimize index.js out.js

Но если только в файл с модулями добавится любой, даже самый маленький класс с export при наличии babel-loader, то пиши пропало. Класс попадет в итоговую сборку в виде функции, которую выплюнул babel. (код)

//module.js

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

export class MyClass {
    print(){
        console.log('find me');
    }
}

Как же так получилось?

Все дело в том, что UglifyJS боится выкинуть что-то лишнее. Оно и понятно: пусть лучше на пару сотен байт больше, только бы не сломалось.

И вот, представьте себе, что UglifyJS получает на вход следующий код:

/* unused harmony export MyClass */
var MyClass = function () {
  function MyClass() {
    babelHelpers.classCallCheck(this, MyClass);
  }

  MyClass.prototype.turn = function print() {
    console.log('find me');
  };

  return MyClass;
}();

MyClass после компиляции babel как-то выбрался за осознания себя как класса. И вообще UglifyJS мало что знает про то, как команда babel видит реализацию классов на ES5. Вот и пасует, оставляя это неведомое никем не используемое безобразие в вашей итоговой сборке.
На это даже есть баг в репозитории webpack, и ребята обещают починить все в 4й версии.
Rollup, кстати, не так давно тоже работал только на примерах с математикой, но в последних версиях ребята починили баг. (сломанный пример, работающий пример).

Мораль

Вот так, купив webpack в том числе и за tree-shaking, я получил околонулевую выгоду в этом направлении. И слово tree-shaking теперь вызывает у меня нервный смех, икоту и неконтролируемый сарказм.

И как теперь быть?

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

Извините, не удержался.

Если серьезно, есть очень простой способ починить ситуацию:

нужно, чтобы webpack добавлял специальную директиву /*#__PURE__*/, которая говорила бы UglifyJS, что вот это неведомое чудище в виде функции вполне себе можно выпиливать.

Ох уж эти костыли.

А выглядит это как-то так:

/* unused harmony export MyClass */
var MyClass  =  /*#__PURE__*/ function () {
  function MyClass() {
    babelHelpers.classCallCheck(this, MyClass);
  }

  MyClass.prototype.turn = function print() {
    console.log('find me');
  };

  return MyClass;
}();

Еще пара итераций и мы изобретем статическую типизацию ;)

Работающий пример

Кстати, новая версия babel 7 уже делает это. К сожалению, она пока еще в бете, что как бы намекает на невозможность использования прямо сейчас. Но если вы смелы и решительны, можно попробовать обновиться.

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

Выводы

  1. Сомневайтесь во всем. Проверяйте информацию. Даже эту статью. Такая практика сэкономит вам кучу нервов и времени.
  2. Экосистема javascript иногда допускает сбои на стыках технологий. Вот и здесь из-за того, что babel ничего не сказал UglifyJS через webpack о свем формате классов для ES5, получилось недопонимание. При этом в babel его уже почти исправили, о чем, конечно же, не знают ребята из webpack и хотят исправить в следующем релизе.

P.S. Напишите в комментариях, если тоже попадались на удочку маркетологов и выбирали модную технологию вместо решения проблем. 

Автор: Алексей Золотых

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js