Оптимизация фронтенда. Часть 2. Чиним tree-shaking в проекте на webpack


    Итак, если специально не чинить, tree-shaking в webpack не работает. Кто не верит, читайте мою предыдущую статью. Если починить очень хочется, то добро пожаловать под кат. Тут есть несколько вариантов, которые я смог подсмотреть, найти придумать.


    Тестовый код


    Все эксперименты я делал на двух файлах, которые выглядят вот так:


    // module.js
    
    class Wheel {
      pump(){ console.log('puuuuf');}
    }
    
    class Rudder {
      turn(){ console.log('turn');}
    }
    
    export {Wheel, Rudder}

    // index.js
    
    import {Wheel} from './module.js';
    
    class Car {
      constructor() {
        this.wheel = new Wheel();
      }
    }
    
    const car = new Car();
    car.wheel.pump();

    Вариант очевидный, кривой: uglify -> babel -> uglify


    Код


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


    Было лень что-то специально писать для webpack, поэтому все шаги я выполнял в консоли через npm-скрипты.


    Я скомпилировал файлы из примера без babel-loader и попробовал пропустить через UglifyJS. UglifyJS упал. Ну не понимает нового синтаксиса javascript!


    Ничего страшного, подумал я, и откопал стюардессу uglify-es. Проблема в том, что babel ничего не знает про минификацию, и на выходе получается неминифицированный код.


    Придется после babel еще раз прогонять UglifyJS.


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


    Вариант разумный: ждать babel 7


    Код


    Вашим кодом пользуются клиенты? Он в продакшне? Если да, то просто запланируйте обновление на новый babel после его выхода. Катастрофы не случится, ведь ваше приложение работало все это время и без tree-shaking.
    Если вы только начинаете разрабатывать проект с нуля и при этом достаточно смелы, то можно уже сейчас подключить тестовую версию. Пока вы работаете над своим приложением, выйдет новая версия babel.
    В любом случае babel – опенсорсный, а значит, вы сами сможете помочь ему выйти раньше.


    Вариант радикальный: Rollup


    Код


    Если вы не сильно погрязли в webpack, возможно, еще не поздно соскочить на Rollup. В случае простых приложений и библиотек Rollup — хороший выбор.


    Вариант радикальный и с костылями, но иногда работает: typescript


    Код


    Можно выкинуть babel и компилировать ваше приложение при помощи typescript. Ведь нам обещали, что любой javascript это уже typescript ;)
    В том месте, где должна стоять директива #__PURE__ typescript оставляет директиву @class, и, как вы понимаете, ничего не стоит написать загрузчик, который меняет этот самый @class на то, что нам нужно.


    module.exports = function(content) {
      return content.replace(/\/\*\* @class \*\//g, '\n /*#__PURE__*/ \n');
    };

    Думаю, что с flow тоже можно что-то придумать. Кстати, пока писал, подумал, что, возможно, и babel тоже можно как-то помочь уже сейчас. Может кто-то из вас знает ответ?


    Вариант радикальный, для фанатов оптимизации: google closure compiler и advanced mode


    Код


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


    Webpack + Babel:
    !function(n){function e(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return n[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var t={};e.m=n,e.c=t,e.d=function(n,t,r){e.o(n,t)||Object.defineProperty(n,t,{configurable:!1,enumerable:!0,get:r})},e.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(t,"a",t),t},e.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},e.p="",e(e.s=0)}([function(n,e,t){"use strict";function r(n,e){if(!(n instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var o=t(1);(new function n(){r(this,n),this.wheel=new o.a}).wheel.pump()},function(n,e,t){"use strict";function r(n,e){if(!(n instanceof e))throw new TypeError("Cannot call a class as a function")}function o(n,e){for(var t=0;t<e.length;t++){var r=e[t];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(n,r.key,r)}}function u(n,e,t){return e&&o(n.prototype,e),t&&o(n,t),n}t.d(e,"a",function(){return c});var c=function(){function n(){r(this,n)}return u(n,[{key:"pump",value:function(){console.log("puuuuf")}}]),n}()}]);

    Rollup:
    !function(){"use strict";var classCallCheck=function(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")},Wheel=function(){function Wheel(){classCallCheck(this,Wheel)}return Wheel.prototype.pump=function(){console.log("puuuuf")},Wheel}();(new function Car(){classCallCheck(this,Car),this.wheel=new Wheel}).wheel.pump()}();

    Google closure compiler в advanced mode

    console.log('puuuuf');


    Думаю, дальше объяснять ничего не нужно. Но есть и проблемы. Основных 2:


    • Требует Java
    • Неудобно настраивать Плохо интегрируется в webpack, который, как вы понимаете, любят далеко не только за tree-shaking.
      Вообще, Google closure compiler (GCC) требует анализа всего вашего кода. Это накладывает свои ограничения, но и дает плюшки в виде оптимизаций, недоступных из Webpack по умолчанию.

    Запускается GCC вот так:


    java -jar node_modules/google-closure-compiler/compiler.jar --compilation_level ADVANCED --language_in=ES6 --js ./src/index.js ./src/module.js > out.dev.js

    Первая проблема вполне решаема, тем более что есть хоть и тормозная, но все-таки версия на javascript.
    О второй проблеме как раз и будет моя следующая статья в этой серии, в ней я постараюсь интегрировать google closure compiler в сборку.


    PS:
    Совсем недавно коллега подсказал, что для GCC вышел плагин для webpack, я запустил, проверил, кода стало явно меньше, но вот tree-shaking так и не заработал. Видимо статья про настройку GCC все-таки нужна.


    Код который у меня получился
    var __wpcc;void 0===__wpcc&&(__wpcc={}),function(c){"use strict";var n;void 0===n&&(n=function(){}),n.p="",n.src=function(c){return n.p+""+c+".out.dev.js"}}.call(this,__wpcc);var __wpcc;void 0===__wpcc&&(__wpcc={}),function(c){"use strict";var n=function(){},o=function(){},t={};n.prototype.pump=function(){window.console.log("puuuuf")},o.prototype.turn=function(){window.console.log("turn")},t.Wheel=n,t.Rudder=o,(new function(){this.wheel=new t.Wheel}).wheel.pump()}.call(this,__wpcc);

    Вместо вывода


    Возможно, у вас возникнет вопрос: а как конкретно все это tree-shaking повлияет на мою сборку?
    Если честно, прямо сейчас я не могу ответить на этот вопрос со всей ответственностью. Есть мнение, что хуже не будет, будет лучше. Насколько? Вопрос сложный. Если вы попробуете, поделитесь в комментариях. Я же, в свою очередь, обещаю держать вас в курсе событий, и как только появятся заслуживающие внимания мысли на этот счет, с удовольствием ими поделюсь.

    • +18
    • 5,8k
    • 7
    Wrike 219,77
    Wrike делает совместную работу над проектами проще
    Поделиться публикацией
    Комментарии 7
    • 0

      Не увидел как у вас сконфигурирован babel-loader, но разве для корректной работы tree-shaking в webpack2 не нужно выключать обработку модулей у babel?
      Типа так:


                          loader: 'babel-loader',
                          options: {
                              presets: [['es2015', {
                                  'modules': false,
                              }]]                        
                          }

      Этим самым import/export обрабатывает сам webpack, а не babel. Или в данном случае все-равно проблема остается?

      • +1
        Проблема все равно остается. Вот так выглядит мой .babelrc. Во всех примерах я его не меняю.
        {
          "presets": [
            [
              "es2015", { "modules": false, loose: true }
            ]
          ],
          "plugins": [
            "external-helpers"
          ]
        }
        
        • 0
          Почему es2015, а не env?
          • +1
            Потому что env зависит от babel 7 beta, чего не очень хочется
            • +1

              Это новая beta-версия preset-env зависит от beta-версии Babel (что логично).


              Стабильный релиз babel-preset-env был отсюда и здесь все зависимости от Babel 6.x

      • 0
        требует java

        Вроде как нет

        • 0
          Я пробовал на эту версию, но она вроде как гораздо медленней, чем java. Поэтому просто в расчет не взял. Но это было давно, возможно что-то изменилось

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое