4 совета для оптимизации webpack-приложения

Всем привет!

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

Кот-фронтендер смотрит на 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%.

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

UPD 11.01.2019: UglifyJsPlugin устарел, webpack сейчас использует TerserWebpackPlugin. Используйте его.

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 еще можно использовать для удаления ненужных полифиллов, которые авторы библиотек заботливо нам подложили.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 17

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

    Открыл для себя anime.js
    15кб, а возможности почти все те же что и у GreenSock.
      0
      У GSAP слишком крутые таймлайны, которые можно вкладывать в другие таймлайны, на которые можно поставить label и начать от него еще один таймлайн)

      + большой форум и крутые плагины, после такого тяжело слезть на что-то другое
      0
      Я собираю скрипты последней версией webpack babel и в сгененрирванных скриптах не нахожу _asyncToGenerator а все на промисах.
      Похоже эта часть уже не актуальна.

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

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

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

      Вы обратили внимание что иллюстрация в начале статьи не имеет отношения к JS/babel/webpack?
        0
        Я собираю скрипты последней версией 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%.
          0
          Ссылка на конфиг есть в моем комментарии. Это все в проекте можно протестировать с учетом всего окружения.

          Сорри ссвлка почему-то ушла. Вот она github.com/apapacy/realworld-react-universal-hot/blob/master/webpack/config.client.js
            0
            Так у вас в коде и нету асинхронных функций, вот таких:
            const test = async () => {
              await fetch('qwe-qwe');
            }
            

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

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

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

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

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

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

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

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

        Only users with full accounts can post comments. Log in, please.