Создание tree-shaking библиотеки с Rollup и Vue

В статье описан процесс оптимизации библиотеки с использованием Rollup и Vue, с акцентом на tree-shaking для удаления неиспользуемого кода и повышения производительности приложений.

Почему не Webpack для создания библиотеки?

Webpack можно использовать для основного приложения (проекта), которое будет использовать нашу UI библиотеку (для которой выбрали Rollup). Мы используем Rollup для компиляции библиотеки, потому что Webpack не поддерживает формат вывода ES6 (пока) Webpack хорошо подойдет для сборки основного проекта, поскольку он поддерживает встроенное встряхивание дерева и может объединять код, отличный от JS. Единственное предостережение относительно алгоритма встряхивания дерева Webpack заключается в том, что он вряд ли может определить, вызывает ли код побочные эффекты при импорте, поэтому в package.json библиотеки следует добавить ключ sideEffects, для которого можно установить значение false, если библиотека не запускает побочные эффекты.
// В библиотеке нет сайд эффектов
{
  …
  "sideEffects": false
}

// Все *.scss, которые при импорте будут вызывать сайд эффекты
{
  …
  "sideEffects": [ "src/**/*.scss" ]
}

Некоторые понятия

Давайте определим некоторые понятия, чтобы пост был понятнее:
  • Tree shaking (встряхивание дерева) - это термин, широко используемый в контексте JavaScript, чтобы описать удаление неиспользуемого кода. Он основан на операторах import и export в ES2015 для определения, были ли кодовые модули экспортированы и импортированы для использования в JavaScript-файлах.
  • Однофайловые Vue компоненты (SFC) - это способ определения компонентов в VueJS с помощью файлов .vue, которые включают шаблон, скрипт и стиль в одном файле. Другие способы определения компонентов VueJS - это обычный JavaScript (с помощью Vue.extent(...)) и JSX.

Почему нужна поддержка tree shaking?

Один из факторов, которые следует учитывать при разработке веб-приложения, - это размер пакета, доставляемого в браузер. Для небольших проектов это может быть незначительно, но для больших проектов это может стать проблемой из-за количества зависимостей и устаревшего кода. При создании внешней библиотеки следует поддерживать удаление неиспользуемого кода, потому что:
  • В крупных проектах часто есть много зависимостей, и по мере роста проекта и изменения требований некоторые из этих зависимостей становятся неиспользуемым кодом, который тем не менее упаковывается, увеличивая размер приложения.
  • При создании больших библиотек (например, библиотек UI компонентов) многие потребители будут использовать только подмножество предоставляемых функций, и если библиотека не поддерживает удаление неиспользуемого кода, то им придется импортировать все функции.

Требования к библиотеке для поддержки tree shaking

Если мы хотим, чтобы наша библиотека была статически анализируема, чтобы сборщики могли удалять неиспользуемый код, мы должны выполнить следующие требования:
  • Она должна быть экспортирована в формате ES6, конкретно с использованием синтаксиса import/export (а не синтаксиса require CommonJS). Таким образом, код статически анализируем, и поэтому можно определить, используется ли код или нет.
  • Он не должен быть упакован (bundled). Это облегчает работу компилятора, изолируя код по модулям и не объединяя все в один файл.
  • Модули, которые мы хотим встряхнуть, не должны вызывать побочных эффектов. Это означает, что они не должны изменять глобальные переменные или вызывать любые другие виды действий с побочными эффектами при импортировании.

Конфигурация Rollup

Обычно настройка Rollup для tree shaking довольно прямолинейна, но в этом случае есть некоторые ограничения:
  • TypeScript нужно скомпилировать в ES6 JavaScript
  • VueJS SFC нужно скомпилировать в ES6 JavaScript
Rollup может сохранить модули в отдельных файлах, но сохраняет оригинальное название (так что файлы TypeScript останутся .ts, то же самое для файлов .vue). Мы фактически получаем файлы TypeScript и Vue, но с содержимым JavaScript. Для этого нам нужно использовать некоторые плагины в Rollup:

Файлы конфигурации

package.json

{
  "name": "your-library-name",
  "version": "0.1.0",
  "module": "dist/index.js",
  "sideEffects": false,
  "scripts": {
    "build": "rollup --config ./config/rollup.config.js",
    "serve": "rollup --config ./config/rollup.config.js --watch",
    "test": "jest --config ./config/jest.config.js --rootDir ."
  },
  "devDependencies": {
    "@betit/rollup-plugin-rename-extensions": "^0.0.4",
    "@types/jest": "^24.0.15",
    "@vue/test-utils": "^1.0.0-beta.29",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-preset-vue": "^2.0.2",
    "jest": "^24.8.0",
    "jest-serializer-vue": "^2.0.2",
    "postcss": "^7.0.17",
    "rollup": "^1.16.7",
    "rollup-plugin-cleaner": "^1.0.0",
    "rollup-plugin-commonjs": "^10.0.1",
    "rollup-plugin-typescript2": "^0.22.0",
    "rollup-plugin-vue": "^5.0.1",
    "standard-changelog": "^2.0.18",
    "ts-jest": "^24.0.2",
    "ts-loader": "^6.0.4",
    "typescript": "^3.5.3",
    "vue": "^2.6.10",
    "vue-jest": "^3.0.4",
    "vue-loader": "^15.7.0",
    "vue-property-decorator": "^8.2.1",
    "vue-template-compiler": "^2.6.10"
  },
  "peerDependencies": {
    "vue": "^2.6.10"
  }
}

rollup.config.js

import vue from 'rollup-plugin-vue'
import typescript from 'rollup-plugin-typescript2'
import renameExtensions from '@betit/rollup-plugin-rename-extensions'
import cleaner from 'rollup-plugin-cleaner'
import commonjs from 'rollup-plugin-commonjs'
export default {
  input: 'index.js',
  output: {
    format: 'esm', // Это то, что говорит rollup использовать ES6 модули
    dir: 'dist'
  },
  external: [ 'vue', 'vue-class-component' ],
  plugins: [
    cleaner({ targets: [ 'dist' ] }),
    commonjs(),
    typescript({ rollupCommonJSResolveHack: true, clean: true }),
    // Это расширение переименовывает .vue и .ts в .js и обновляет импорты
    renameExtensions({
      include: ['**/*.ts', '**/*.vue'],
      mappings: { '.vue': '.vue.js', '.ts': '.js' }
    }),
    vue()
  ],
  // Предотвращает бандлинг, но не переименовывает файлы
  preserveModules: true
}