Практика оптимизации производительности Tree-Shaking — Практика

CSS Webpack Babel PostCSS
Практика оптимизации производительности Tree-Shaking — Практика

предыдущий постПрактика оптимизации производительности Tree-Shaking — принципыВводится принцип встряхивания деревьев.Эта статья в основном знакомит с практикой встряхивания деревьев.

图标

3. практика встряхивания деревьев

Выпущен Webpack2, объявляющий о поддержке tree-shaking, и выпущен webpack 3, поддерживающий расширение области действия, а сгенерированные файлы пакетов меньше. Перед обновлением веб-пакета Зенг предполагает, что наша производительность значительно улучшится, и мы полны ожиданий от обновления. На самом деле это факт

После апгрейда размер бандл-файла сильно не уменьшился.Был большой психологический разрыв на тот момент,и тогда я пошел изучать почему эффект не идеальный.По причинам см.Практика оптимизации производительности Tree-Shaking — принципы.

Оптимизацию по-прежнему необходимо продолжать, хотя встряхивание дерева, которое поставляется с инструментом, не может удалить слишком много бесполезного кода, все же есть вещи, которые можно сделать с точки зрения удаления бесполезного кода. Мы делаем некоторую оптимизацию с трех аспектов.



(1) Оптимизация ссылок на библиотеки компонентов

Сначала рассмотрим вопрос

Когда мы используем библиотеку компонентов, import {Button} из 'element-ui', по сравнению с Vue.use(elementUI), уже зависит от производительности и является рекомендуемой практикой, но если мы напишем это в форме справа, Конкретная разница между ссылками на файлы и упаковкой очень велика.Взяв antd в качестве примера, объем пакета справа уменьшен примерно на 80%.

Эта ссылка также является побочным эффектом, веб-пакет не может изменять дерево других компонентов. Поскольку сам инструмент не может этого сделать, мы можем использовать инструмент для автоматической замены кода слева на код справа. Этот инструмент и сама библиотека также предоставляются. Я сделал несколько модификаций на основе инструментов antd без какой-либо настройки и изначально поддерживает нашу собственную библиотеку компонентов,wuiа такжеxcuiи некоторые другие часто используемые библиотеки

babel-plugin-import-fix , чтобы сузить область ссылок

图标
lin-xi/babel-plugin-import-fix


Далее описывается принцип

Это плагин Babel. Babel преобразует код ES6 в AST Абстрактное синтаксическое дерево через Core Babylon, а затем плагин переходит на синтаксическое дерево, чтобы найти заявления, такие как Import {кнопка} из «Element-UI», преобразует его, и, наконец, восстанавливает кода

babel-plugin-import-fix по умолчанию поддерживает antd, element, meterial-UI, wui, xcui и d3, просто настройте сам плагин в .babelrc.

.babelrc

{
  "presets": [
    ["es2015", { "modules": false }], "react"
  ],
  "plugins": ["import-fix"]
}

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



(2) Встряхивание дерева CSS

Упомянутое ранее встряхивание деревьев предназначено только для js-файлов. Благодаря статическому анализу бесполезный код удаляется в максимально возможной степени. Можем ли мы сделать встряхивание деревьев для CSS?

С популяризацией CSS3, LESS, SASS и других языков предварительной обработки CSS нельзя игнорировать долю файлов CSS во всем проекте. При непрерывной итерации больших функций проекта в CSS может быть бесполезный код. Я реализовал плагин webpack, чтобы решить эту проблему и выяснить бесполезный код кода css.

webpack-css-treeshaking-plugin, встряхивание дерева css

图标
webpack-css-treeshaking-plugin


Далее описывается принцип

Общая идея заключается в том, чтобы обойти все селекторы селекторов в файлах css, а затем перейти ко всему коду js для сопоставления, если селектор не отображается в коде, считается, что селектор является бесполезным кодом.

Первый вопрос: как изящно обойти все селекторы? Очень сложно использовать регулярные выражения для соответствия сегментации?

Babel — счастливая звезда мира js, на самом деле, мир css тоже имеет преимущество, то есть postCss.

PostCSS предоставляет синтаксический анализатор, который преобразует CSS в абстрактное синтаксическое дерево AST. Затем мы можем написать различные плагины, обработать абстрактное синтаксическое дерево и, наконец, сгенерировать новые файлы css, чтобы достичь цели точной модификации css.

В целом это еще один подключаемый модуль веб-пакета.Схема архитектуры выглядит следующим образом:

Основной процесс:

  • Плагин отслеживает событие завершения компиляции webapck.После завершения компиляции webpack находит все файлы css и js из компиляции
apply (compiler) {
    compiler.plugin('after-emit', (compilation, callback) => {

      let styleFiles = Object.keys(compilation.assets).filter(asset => {
        return /\.css$/.test(asset)
      })

      let jsFiles = Object.keys(compilation.assets).filter(asset => {
        return /\.(js|jsx)$/.test(asset)
      })

     ....
}
  • Отправить все файлы css в postCss для обработки и найти бесполезный код
   let tasks = []
    styleFiles.forEach((filename) => {
        const source = compilation.assets[filename].source()
        let listOpts = {
          include: '',
          source: jsContents,  //传入全部js文件
          opts: this.options   //插件配置选项
        }
        tasks.push(postcss(treeShakingPlugin(listOpts)).process(source).then(result => {       
          let css = result.toString()  // postCss处理后的css AST  
          //替换webpack的编译产物compilation
          compilation.assets[filename] = {
            source: () => css,
            size: () => css.length
          }
          return result
        }))
    })
  • postCss обход, сопоставление, удаление процесса
 module.exports = postcss.plugin('list-selectors', function (options) {
    // 从根节点开始遍历
    cssRoot.walkRules(function (rule) {
      // Ignore keyframes, which can log e.g. 10%, 20% as selectors
      if (rule.parent.type === 'atrule' && /keyframes/.test(rule.parent.name)) return
      
      // 对每一个规则进行处理
      checkRule(rule).then(result => {
        if (result.selectors.length === 0) {
          // 选择器全部被删除
          let log = ' ✂️ [' + rule.selector + '] shaked, [1]'
          console.log(log)
          if (config.remove) {
            rule.remove()
          }
        } else {
          // 选择器被部分删除
          let shaked = rule.selectors.filter(item => {
            return result.selectors.indexOf(item) === -1
          })
          if (shaked && shaked.length > 0) {
            let log = ' ✂️ [' + shaked.join(' ') + '] shaked, [2]'
            console.log(log)
          }
          if (config.remove) {
            // 修改AST抽象语法树
            rule.selectors = result.selectors
          }
        }
      })
    })

checkRule обрабатывает основной код каждого правила.

let checkRule = (rule) => {
      return new Promise(resolve => {
        ...
        let secs = rule.selectors.filter(function (selector) {
          let result = true
          let processor = parser(function (selectors) {
            for (let i = 0, len = selectors.nodes.length; i < len; i++) {
              let node = selectors.nodes[i]
              if (_.includes(['comment', 'combinator', 'pseudo'], node.type)) continue
              for (let j = 0, len2 = node.nodes.length; j < len2; j++) {
                let n = node.nodes[j]
                if (!notCache[n.value]) {
                  switch (n.type) {
                    case 'tag':
                      // nothing
                      break
                    case 'id':
                    case 'class':
                      if (!classInJs(n.value)) {
                        // 调用classInJs判断是否在JS中出现过
                        notCache[n.value] = true
                        result = false
                        break
                      }
                      break
                    default:
                      // nothing
                      break
                  }
                } else {
                  result = false
                  break
                }
              }
            }
          })
          ...
        })
        ...
      })
    }

Видно, что на самом деле я имею дело только с селектором id и селектором класса, id и class имеют относительно небольшие побочные эффекты, и возможность возникновения исключений стиля относительно невелика.

Чтобы судить о том, появился ли css снова в js, стоит использовать обычное сопоставление.

По сути можно продолжить оптимизацию и в дальнейшем.Например для селектора класса тега можно настроить,появился ли он в html,jsx,шаблоне.Если появился,то его можно считать бесполезным кодом если не появилось.

Конечно, есть некоторые предпосылки и ограничения для правильной работы плагина. Мы можем динамически изменять css в коде, например, в react и vue, мы можем написать так

Это рекомендуемый способ. Селектор отображается в коде как символ или имя переменной. Последующая динамическая генерация селектора приведет к сбою сопоставления.

render(){
  this.stateClass = 'state-' + this.state == 2 ? 'open' : 'close'
  return <div class={this.stateClass}></div>
}

где легко избежать

render(){
  this.stateClass = this.state == 2 ? 'state-open' : 'state-close'
  return <div class={this.stateClass}></div>
}

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


(3) Дедупликация файлов пакетов webpack

Если такой же модуль существует в пакетном файле, упакованном webpack, это тоже своего рода бесполезный код. также следует удалить

В первую очередь нам нужен инструмент, способный качественно анализировать файлы бандлов, находить проблемы и видеть эффект оптимизации.

Плагин webpack-bundle-analyzer может полностью удовлетворить наши потребности, он умеет графически отображать размер каждого компонента всех модулей в комплекте.

Во-вторых, необходимость извлечения общих модулей. CommonsChunkPlugin — самый известный плагин для предоставления общих модулей. В первые дни я не до конца понимал его функцию и не играл ее в полной мере.

Ниже описано правильное использование CommonsChunkPlugin.

Автоматически извлекать все node_modules или модули, на которые есть ссылки более двух раз

minChunks может принимать значение или функцию, если это функция, вы можете настроить правила упаковки

Но после использования конфигурации, описанной выше, вы не можете сидеть сложа руки и расслабляться. Потому что эта конфигурация может извлекать только общие модули во всех файлах, упакованных записью. Реальность такова, что с улучшенной производительностью мы будем загружать по запросу.Благодаря методу import(...), предоставляемому веб-пакетом, этот файл по запросу не существует в записи, поэтому асинхронный модуль, загружаемый по запросу. извлечено.

Как извлечь общие модули в асинхронных модулях, загружаемых по запросу?

Настройте другой CommonsChunkPlugin, добавьте асинхронный атрибут, асинхронный может принимать логические или строковые значения. Когда это строка, по умолчанию используется имя выходного файла.

имена — это имена всех асинхронных модулей

Также есть некоторые сведения об именовании асинхронных модулей. Я делаю это так:

const Edit = resolve => { import( /* webpackChunkName: "EditPage" */ './pages/Edit/Edit').then((mod) => { resolve(mod.default); }) };
const PublishPage = resolve => { import( /* webpackChunkName: "Publish" */ './pages/Publish/Publish').then((mod) => { resolve(mod); }) };
const Models = resolve => { import( /* webpackChunkName: "Models" */ './pages/Models/Models').then((mod) => { resolve(mod.default); }) };
const MediaUpload = resolve => { import( /* webpackChunkName: "MediaUpload" */ './pages/Media/MediaUpload').then((mod) => { resolve(mod); }) };
const RealTime = resolve => { import( /* webpackChunkName: "RealTime" */ './pages/RealTime/RealTime').then((mod) => { resolve(mod.default); }) };

Да, добавить комментарий к импорту. /* webpackChunkName: "EditPage" */ хоть и выглядит неудобно, но работает.

Разместите сравнительную диаграмму эффекта оптимизации проекта

Эффект оптимизации все еще относительно очевиден.

бандл перед оптимизацией
Оптимизированный пакет



Последний вопрос для размышления:

Должны ли разные модули ввода или асинхронные модули, загружаемые по запросу, извлекать общие модули?

Это зависит от сцены. Например, модули загружаются онлайн. Если степень детализации общих модулей слишком мала, это приведет к тому, что для первого экрана домашней страницы потребуется больше файлов. Загрузка страницы уровня 1 или уровня 3 будет значительно улучшилась. Следовательно, это необходимо взвесить в соответствии с бизнес-сценарием, чтобы контролировать степень детализации извлечения общих модулей.

Сценарий мобильного приложения Baidu Takeaway выглядит следующим образом: все наши мобильные страницы обрабатываются в автономном режиме. После автономной работы загрузите локальный файл js, который не имеет ничего общего с сетью, и в основном игнорируйте размер файла, поэтому уделите больше внимания размеру всего автономного пакета. Чем меньше оффлайн-пакет, тем меньше трафика потребляет пользователь, и пользовательский опыт лучше, поэтому офлайн-сценарий очень подходит для извлечения общих модулей с наименьшей степенью детализации, то есть всех входных модулей и асинхронно загружаемых модулей. извлекаются ссылки со ссылками больше 2. В результате получается наименьший выходной файл и наименьший автономный пакет.

20 января я поделюсь в Nuggets "Практикой фронтенда Baidu Takeaway Offline" Если вам интересно, то можете обратить внимание.


Упомянутые текстовые плагины являются открытым исходным кодом, ссылками коллекции для приветствия биржи, приветствовали штамп ❤

图标

lin-xi/babel-plugin-import-fix