javascript

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

  • пятница, 24 ноября 2017 г. в 03:13:54
https://habrahabr.ru/company/wrike/blog/342964/
  • Разработка веб-сайтов
  • Клиентская оптимизация
  • JavaScript
  • Блог компании Wrike



Итак, если специально не чинить, 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 повлияет на мою сборку?
Если честно, прямо сейчас я не могу ответить на этот вопрос со всей ответственностью. Есть мнение, что хуже не будет, будет лучше. Насколько? Вопрос сложный. Если вы попробуете, поделитесь в комментариях. Я же, в свою очередь, обещаю держать вас в курсе событий, и как только появятся заслуживающие внимания мысли на этот счет, с удовольствием ими поделюсь.