Всем привет!

За время моей работы с вебпаком у меня накопилась пара интересных советов, которые помогут вам приготовить отлично оптимизированное приложение. Приступим!

Кот-фронтендер смотрит на webpack и говорит 'Белиссимо'


1. Используйте fast-async вместо regenerator-runtime


Обычно, разработчики используют @babel/preset-env, чтобы преобразовывать весь современный синтаксис в ES5.

С этим пресетом пайплайн преобразований асинхронных функций выглядит так:
Исходная асинхронная функция -> Генератор -> Функция, использующая regenerator-runtime

Пример
1. Исходная асинхронная функция

const test = async () => {
  await fetch('/test-api/', { method: 'GET' });
}

2. Генератор

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

const test = (() => {
  var _ref = _asyncToGenerator(function* () {
    yield fetch('/test-api/', { method: 'GET' });
  });

  return function test() {
    return _ref.apply(this, arguments);
  };
})();

3. Функция, использующая regenerator-runtime

'use strict';

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

var test = function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return fetch('/test-api/', { method: 'GET' });

          case 2:
          case 'end':
            return _context.stop();
        }
      }
    }, _callee, undefined);
  }));

  return function test() {
    return _ref.apply(this, arguments);
  };
}();


С fast-async пайплайн упрощается до:
Исходная асинхронная функция -> Функция, использующая промисы

Пример
1. Исходная асинхронная функция

const test = async () => {
  await fetch('/test-api/', { method: 'GET' });
}

2. Функция, использующая промисы

var test = function test() {
  return new Promise(function ($return, $error) {
    return Promise.resolve(fetch('/test-api/', {
      method: 'GET'
    })).then(function ($await_1) {
      try {
        return $return();
      } catch ($boundEx) {
        return $error($boundEx);
      }
    }, $error);
  });
};


Благодаря этому, теперь у нас нет regenerator-runtime на клиенте и лишних оберток от трансформаций.

Чтобы подвести fast-async в свой проект, надо:

1. Установить его

npm i fast-async

2. Обновить конфиг бабеля

// .babelrc.js
module.exports = {
  "presets": [
    ["@babel/preset-env", {
      /* ... */
      "exclude": ["transform-async-to-generator", "transform-regenerator"]
    }]
  ],
  /* ... */
  "plugins": [
    ["module:fast-async", { "spec": true }],
    /* ... */
  ]
}

У меня эта оптимизация уменьшила размер js файлов на 3.2%. Мелочь, а приятно :)

2. Используйте loose трансформации


Без специальной настройки @babel/preset-env пытается сгенерировать как можно более близкий к спецификации код.

Но, скорее всего, ваш код не настолько плох и не использует все возможные крайние случаи ES6+ спецификации. Тогда весь лишний оверхед можно убрать, включив loose трансформации для preset-env:

// .babelrc.js
module.exports = {
  "presets": [
    ["@babel/preset-env", {
      /* ... */
      "loose": true,
    }]
  ],
  /* ... */
}

Пример того, как это работает, можно найти тут.

В моем проекте это уменьшило размер бандла на 3.8%.

3. Настройте минификацию js и css руками


Дефолтные настройки для минификаторов содержат только те трансформации, которые не смогут ничего сломать у программиста. Но мы ведь любим доставлять себе проблемы?
Попробуйте почитать настройки минификатора js и своего минификатора css (я использую cssnano).

Изучив доки, я сделал такой конфиг:

// webpack.config.js
const webpackConfig = {
  /* ... */
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        uglifyOptions: {
          compress: {
            unsafe: true,
            inline: true,
            passes: 2,
            keep_fargs: false,
          },
          output: {
            beautify: false,
          },
          mangle: true,
        },
      }),
      new OptimizeCSSPlugin({
        cssProcessorOptions: {
          "preset": "advanced",
          "safe": true,
          "map": { "inline": false },
        },
      }),
    ],
  },
};
/* ... */

В результате размер js файлов уменьшился на 1.5%, а css — на 2%.

Может, у вас получится лучше?

4. Используйте null-loader для удаления ненужных зависимостей


У разработчиков gsap получилась отличная библиотека для создания анимаций. Но из-за того, что она берет свое начало еще из 2008 года, в ней остались некоторые особенности.

А именно вот эта. Благодаря ней TweenMax тянет за собой 5 плагинов и easePack, которые юзать совершенно необязательно.

У себя я заметил три лишних плагина и выпилил их с помощью null-loader:

// webpack.config.js
const ignoredGSAPFiles = ['BezierPlugin', 'DirectionalRotationPlugin', 'RoundPropsPlugin'];

const webpackConfig = {
  /* ... */
  module: {
    rules: [
      /* ... */
      {
        test: /\.js$/,
        include: ignoredGSAPFiles.map(fileName => resolve('node_modules/gsap/' + fileName)),
        loader: 'null-loader',
      },
    ]
  },
};
/* ... */

И 106 кб превращаются в 86. Та-да!

Null-loader еще можно использовать для удаления ненужных полифиллов, которые авторы библиотек заботливо нам подложили.

Комментарии (17)


  1. noodles
    03.10.2018 18:06

    У разработчиков gsap получилась отличная библиотека для создания анимаций. Но из-за того, что она берет свое начало еще из 2008 года, в ней остались некоторые особенности.

    Открыл для себя anime.js
    15кб, а возможности почти все те же что и у GreenSock.


    1. AndreasCag Автор
      03.10.2018 18:35

      У GSAP слишком крутые таймлайны, которые можно вкладывать в другие таймлайны, на которые можно поставить label и начать от него еще один таймлайн)

      + большой форум и крутые плагины, после такого тяжело слезть на что-то другое


  1. apapacy
    03.10.2018 18:38

    Я собираю скрипты последней версией webpack babel и в сгененрирванных скриптах не нахожу _asyncToGenerator а все на промисах.
    Похоже эта часть уже не актуальна.

    Ручная настройка минификации js тоже спорный вопрос т.к. в webpack 4 она включена по умолчанию в зависимости от окружения (дейтсвует на проде).

    Наиболее эффективный способ «минимизации» когда речь идет не об нескольких процентах, а об уменьшении скрипта в несколько раз — этоcode splitting

    Помимо уменьшения это позволяет также уменьшить время на разбор скрипта который в большем приложении подгружается по мере необходимости

    Вы обратили внимание что иллюстрация в начале статьи не имеет отношения к JS/babel/webpack?


    1. AndreasCag Автор
      03.10.2018 19:06

      Я собираю скрипты последней версией webpack babel и в сгененрирванных скриптах не нахожу _asyncToGenerator а все на промисах.
      Похоже эта часть уже не актуальна.

      Хмм, тестанул на webpack 4.20.2 + babel/preset-env 7.1.0, в билде присутствует regenerator-runtime.
      Можно конфиг бабеля и пример трансформации?

      Ручная настройка минификации js тоже спорный вопрос т.к. в webpack 4 она включена по умолчанию в зависимости от окружения (дейтсвует на проде).

      Я и хочу, чтобы в проде вместо дефолтных настроек минификации были кастомные, написанные лично программистом :)
      По умолчанию, в проде вебпак вставит внутрь optimization.minimizer вот такое: new UglifyJsPlugin(), а если разрешить минификатору делать unsafe трансформации и сделать два круга минификации, то после 10 минут настройки бандл ужмется на 1-2%.


      1. apapacy
        03.10.2018 19:12

        Ссылка на конфиг есть в моем комментарии. Это все в проекте можно протестировать с учетом всего окружения.

        Сорри ссвлка почему-то ушла. Вот она github.com/apapacy/realworld-react-universal-hot/blob/master/webpack/config.client.js


        1. AndreasCag Автор
          03.10.2018 19:23

          Так у вас в коде и нету асинхронных функций, вот таких:

          const test = async () => {
            await fetch('qwe-qwe');
          }
          

          А есть только обычные функции, работающие с промисами.


          1. apapacy
            03.10.2018 19:35

            async/await как раз исползуется на фронте во всех практически компонентах см. github.com/apapacy/realworld-react-universal-hot/blob/88daca208b47bee73e0aa891c12a0a2f4bd2161c/src/react/pages/article.js#L36


            1. AndreasCag Автор
              03.10.2018 19:50
              +1

              Ага, понял. Собрал проект, у вас везде есть regeneratorRuntime. Можете поискать по `regeneratorRuntime` или `.mark`


              1. apapacy
                03.10.2018 20:30

                Да regeneratorRuntime присутсвует.
                На всякий случай добавил предложенный Вами плагин в код проекта.


  1. dumistoklus
    03.10.2018 19:00

    loose трансформации нельзя навесить на все плагины при использовании babel 7, которые недавно релизнулся. Эта опция была в нем удалена. Нужно перечислять все плагины и в каждом писать loose: true. Что, конечно, мега неудобно


    1. Rulexec
      03.10.2018 22:38

      Опции babel'а в js-файле. Какие проблемы перед тем как экспортировать объект с опциями пройтись по массиву плагинов и в каждый объект добавить поле?


      1. dumistoklus
        04.10.2018 02:22

        Все просто: мы не управляем тем какие плагины используются, поэтому ничего о них не знаем. Нет никакого массива плагинов. Вернее есть, но в нем только один плагин — babel-preset-env: он определяет по списку браузеров browserlist уровень транспиляции, и подгружает те плагины, которые необходимы. При этом сам babel-preset-env также не имеет опции loose


        1. AndreasCag Автор
          04.10.2018 08:10

          Preset-env последней версии имеет loose опцию и успешно ей пользуется.


          1. dumistoklus
            04.10.2018 16:58

            Да, вы правы. Я ошибся с loose опцией


  1. KhodeN
    04.10.2018 21:55

    А чем null-loader лучше встроенного плагина IgnorePlugin?


    1. AndreasCag Автор
      04.10.2018 22:32

      Он не лучше и не хуже, они используются для разных вещей.

      Если в случае с GSAP вы станете использовать IgnorePlugin, то получите ошибку, так как GSAP попытается подтянуть модуль, которого нету. IgnorePlugin его вырезал.

      IgnorePlugin нужен для того, чтобы вырезать из собранного бандла лишние модули, которые вошли в него из-за dynamic-require (Хороший пример с локализацией moment'а у вас по ссылке).

      А null-loader нужен, чтобы преобразовать уже зареквайреный модуль в пустое место.


      1. KhodeN
        04.10.2018 23:21

        Спасибо, в том числе за статью!