Максимально оптимизировать упаковку веб-пакетов_20180619

оптимизация производительности

задний план:

Перед запуском проекта он был оптимизирован для упаковки веб-пакетов, но был пройден, когда оптимизация сети была проведена позже.webpack-bundle-analyzerЭтот плагин обнаруживает, что некоторые общедоступные js-файлы неоднократно упаковываются в js-код бизнес-кода. Хотя размер этих кодов невелик, для максимальной оптимизации я все же хочу их оптимизировать. Самый большой выигрыш в этом процессе — сделать себяwebpack4.xСоответствующие элементы конфигурации более знакомы, и вы можете использовать webpack для простой реализации нужного метода упаковки.

Я помню, как бывший старший коллега сказал что-то о внешней оптимизации:Front-end оптимизация — это компромисс, сделанный после взвешивания различных «за» и «против».

Результаты оптимизации:

Давайте сначала посмотрим на результаты оптимизации, потому что проект представляет собой режим упаковки с несколькими входами (Леса проекта нажмите здесь) У каждой страницы есть ссылка, и у каждой страницы будет свой файл js.

Результат выглядит следующим образом:

  • js уменьшение размера кода: 20kb+
  • Сокращенное время подключения к сети: 500 мс+

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

Статистика до оптимизации

Оптимизированная статистика

Оптимизация времени анализа сети и времени выполнения

1. Добавьте предварительное разрешение DNS

В теге head файла html укажите имя домена, чтобы включить предварительное разрешение DNS, и добавьте предварительное разрешение через метатег. Пример кода выглядит следующим образом:

    <!--告诉浏览器开启DNS 预解析-->
    <meta http-equiv="x-dns-prefetch-control" content="on" />
    <!--添加需要预解析的域名-->
    <link rel="dns-prefetch" href="//tracker.didiglobal.com">
    <link rel="dns-prefetch" href="//omgup.didiglobal.com">
    <link rel="dns-prefetch" href="//static.didiglobal.com">

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

После добавления кода предварительного разрешения DNS его можно четко найти после сравнения с картинкой выше.tracker.didiglobal.comЭто доменное имя было заранее проанализировано перед загрузкой. Если этот файл js влияет на отрисовку страницы (например, страница загружается по запросу), он может улучшить производительность отрисовки страницы.

2. Задержка выполнения кода, влияющего на отрисовку страницы

В процессе разработки мобильного терминала мы обычно обращаемся к каким-то сторонним js-зависимостям, например, к вызову клиентского метода jsbridge.client.jsи сервис точки доступаconsole.log.js.

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

На картинке ниже — статус-кво нашего проекта до оптимизации, а светло-зеленая вертикальная линия — время начала рендеринга. Из рисунка ниже видно, что мы ссылаемся на клиентский методfusion.jsи пунктирomega.js(оба js помещаются в тег head) оба влияют на время начала рендеринга страницы.

На самом деле, нашему бизнес-сценарию не нужны результаты выполнения этих js до того, как страница будет отрисована, так почему бы нам не сделать эти js асинхронно загружаемыми?

Асинхронная загрузка js заключается в правильном выборе времени и использовании динамического созданияscriptПо пути тегов на страницу загружается нужный js. Наш проект использует vue, мы выбираем в жизненном цикле vuemountedЗагрузите js, который нужно запустить, и загрузите его там, где нам нужно вызвать клиентский метод.fusion.jsПри асинхронной загрузке обратного вызова библиотеки пользовательская операция выполняется после загрузки js.

Вот простой пример кода:


export default function executeOmegaFn(fn) {
    // 动态加载js到当前的页面中来,并且当js加载完之后执行回调,注意需要判断js是否已经在当前环境加载过了
    scriptLoader('//xxx.xxxx.com/static/tracker_global/2.2.2/xxx.min.js', function () {
        fn && fn();
    });
}

// 异步加载 需要加载的js
mounted() {
    executeOmegaFn();
},

Следующее изображение представляет собой эффект после модификации.Можно обнаружить, что время начала рендеринга страницы раньше, а время завершения рендеринга страницы раньше, чем на изображении выше.2sо. Хорошо видно, что рендеринг страницы и время загрузки и выполнения омеги никак не влияют друг на друга.

Суммировать:

  1. Добавьте блок кода предварительного разрешения DNS доменного имени в файл шаблона html, чтобы браузер мог предварительно разрешить доменное имя загружаемого статического файла заранее. Когда вам нужно загрузить статические ресурсы, увеличьте скорость загрузки статических файлов.
  2. Задерживая загрузку и выполнение js-файлов, которые не требуются для рендеринга на первом экране, время начала рендеринга страницы увеличивается для повышения скорости рендеринга на первом экране.

Оптимизировать вывод веб-пакета

1. Оптимизируйте дублирование кода и упаковку

По умолчанию webpack4.x находится вproductionВ режиме код будет встряхиваться деревом. Но после прочтения этой статьи вы найдете это в большинстве случаев,tree shakingНет возможности удалить повторяющийся код.Ваш Tree-Shaking бесполезен

В моем проекте есть каталог lib с библиотекой функций, написанной мной в соответствии с потребностями бизнеса.webpack-bundle-analyzerЯ обнаружил, что он неоднократно упаковывался в js-файл нашего бизнес-кода. На следующем рисунке показан файл js, включенный в упакованный бизнес-код, вы можете видетьlibСодержимое каталога многократно упаковано

Когда вы видите эту ситуацию, давайте поговорим о вашем собственном плане оптимизации:

  • Упакуйте зависимости в каталоге node_modules в одинvendorполагаться;
  • Отдельно упакуйте написанную вами библиотеку функций в директории lib и common в общую;
  • Упакуйте зависимую стороннюю библиотеку компонентов по мере необходимости, если вы используете более крупные компоненты в библиотеке компонентов, например компоненты date-picer и scroll в cube-ui. Если он используется только один раз, он будет упакован в js-файл своей собственной страницы, на которую ссылаются, а если на него ссылаются несколько страниц, он будет упакован в общий. ПозадиОптимизация сторонних зависимостейСтратегия оптимизации и упаковки в этой части будет подробно представлена.

Давайте взглянем на мой конфигурационный файл webpack для распаковки, подробности смотрите в комментариях:

splitChunks: {
    chunks: 'all',
    automaticNameDelimiter: '.',
    name: undefined,
    cacheGroups: {
        default: false,
        vendors: false,
        common: {
            test: function (module, chunks) {
                // 这里通过配置规则只将 common lib  cube-ui 和cube-ui 组件scroll依赖的better-scroll打包进入common中
                if (/src\/common\//.test(module.context) ||
                    /src\/lib/.test(module.context) ||
                    /cube-ui/.test(module.context) ||
                    /better-scroll/.test(module.context)) {
                    return true;
                }
            },
            chunks: 'all',
            name: 'common',
            // 这里的minchunks 非常重要,控制cube-ui使用的组件被超过几个chunk引用之后才打包进入该common中否则不打包进该js中
            minChunks: 2,
            priority: 20
        },
        vendor: {
            chunks: 'all',
            test: (module, chunks) => {
                // 将node_modules 目录下的依赖统一打包进入vendor中
                if (/node_modules/.test(module.context)) {
                    return true;
                }
            },
            name: 'vendor',
            minChunks: 2,
            // 配置chunk的打包优先级,这里的数值决定了node_modules下的 cube-ui 不会打包进入 vendor 中
            priority: 10,
            enforce: true
        }
    }
}

Я написал js в проекте, чтобы единообразно представить компоненты кубического пользовательского интерфейса, которые должен использовать весь проект. специфическийкод здесь,См. здесь метод вызова.

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


/**
 * @file cube ui 组件引入统一配置文件 建议这里只引入每个页面使用的基础组件,对于复杂的组件比如scroll datepicer组件
 * 在页面中单独引入,然后在webpack中同过 minChunk 来指定当这些比较大的组件超过 x 引用数时才打进common中否则单独打包进页面的js中
 * @date 2019/04/02
 * @author hpuhouzhiqiang@gmail.com
 */

/* eslint-disable  */
import Vue from 'vue';
import {
    Style,
    Toast,
    Loading,
    // 这里去除 scroll是在页面中单独引入,以使webpack打包时可以根据引用chunk选择是否将该组件打包进入页面的js中还是选择打包进入common中
    // Scroll,
    createAPI
} from 'cube-ui';


export default function initCubeComponent() {
    Vue.use(Loading);
    // Vue.use(Scroll);
    createAPI(Vue, Toast, ['timeout'], true);
}

В настоящее время только страница pay_history в проекте использует компонент прокрутки cube-ui, который упакован в js бизнес-кода отдельно, поэтому js этой страницы больше.

Когда более двух страниц используют компонент прокрутки, он будет автоматически упакован в общий в соответствии с конфигурацией веб-пакета. На следующей картинке результат упаковки, размер js страницы уменьшен, а объем файла commonjs стал больше.

Суммировать:

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

2. Удалите ненужный импорт

Иногда я не обращаю внимания при написании кода, ссылаюсь на стороннюю зависимость через импорт, но не использую ее в итоге или мне не нужно комментировать исполняемое выражение при выходе в интернет.importзаявление, результат упаковки также будет включать импортированный файл js. Например следующий код:


import VConsole from 'vconsole';
// 测试的时候我们可能打开了下面的注释,但是在上线的时候只是注释了下面的代码,webpack打包的时候仍然会将vconsole打包进目标js中。
// var vConsole = new VConsole();

Суммировать:

  • Определите недопустимый оператор импорта. Если нет функции или выражения, импортированного с помощью импорта, закомментируйте или удалите оператор импорта напрямую.

3. оптимизация конфигурации babel-preset-env и autoprefix

В настоящее время использование комбинации babel + ES6 для написания внешнего кода используется редко.babel-polyfill. Основная причина в том, что это загрязнит глобальные переменные и введет полифилл в полном объеме, а упакованный целевой js-файл будет очень большим.

В большинстве случаев для полифилинга используется babel-preset-env. Более разумный или более продвинутый подход — использовать онлайн-сервис полифилла,Ссылка на ссылку

В использованииpreset-envПри этом в большинстве случаев игнорируется настройка списка совместимых браузеров. Или ищите непосредственно из сети в конфигурацию, а не напрямую копируйте результаты вывода, вставляйте их. На самом деле эта конфигурация повлияет на размер нашего файла js.

При использованииautoprefixПри автоматическом добавлении префиксов поставщиков в CSS вам также необходимо настроить список браузеров. Этот список конфигурации также влияет на размер файла css.официальная документация browserslist

Например, телефоны с Windows Phone сейчас почти вымерли, и нет необходимости рассматривать совместимость с телефонами pc и wp для текущих мобильных проектов.Можем ли мы удалить префикс поставщика полифилла или css при добавлении-ms-Что с префиксом, так как его настроить?

Моя конфигурация выглядит следующим образом:

"browsers": [
    "> 1%",
    "last 2 versions",
    "iOS >= 6.0",
    "not ie > 0",
    "not ie_mob  > 0",
    "not dead"
]

Вот краткое упоминание о важности правильного использования новых функций CSS. Код ниже — это то, что я видел в одном из наших старых проектов. На самом деле проблемы на первый взгляд нет, а при просмотре в современных браузерах есть проблемы?

.example {
    display: flex;
    display: -webkit-box;
}

.test {
   flex:1
}

Это написание правильноегибкий макетПроблемы, вызванные несогласованным синтаксическим анализом. в хроме.exampleэффективныйdisplay: -webkit-boxЭтот переходный период макета flexbox написан. существует.testдействуетflex:1И это новый стандартный способ письма. Вызывает проблемы с отображением макета.

код после автопрефикса

.example {
    display: -ms-flexbox;
    display: flex;
    display: -webkit-box;
}

.test {
   -webkit-box-flex:1;
       -ms-flex:1;
           flex:1
}

То же самое также вызовет проблему перед автопрефиксом выше, макет будет неправильным.

Суммировать:

  1. Добавьте определенные конфигурации полифилла в соответствии с собственными бизнес-сценариями.
  2. Если вы используете новые функции css3 и используете autoprefix для автоматического добавления префиксов поставщиков, вам нужно использовать только последнюю стандартную запись в исходном коде.

4. встроенный файл времени выполнения веб-пакета

Используйте веб-пакет для компиляции кода. Когда код генерирует несколько фрагментов, как веб-пакет загружает эти фрагменты?

WebPack реализовал загрузчик модулей для поддержания взаимосвязей между различными модулями (статьи WebPack называют его модулем времени выполнения). Логирование различных модулей идентифицируется строковым номером (специально для написания простого DEMO, чтобы увидеть результат компиляции).

Когда код файла будет изменен, эта строка чисел изменится в модуле выполнения.Если эта часть кода не обрабатывается при упаковке веб-пакета, она будет выведена по умолчанию.vendorв коде. В результате, пока код изменяется, хэш сгенерированного файла поставщика будет меняться, и нет возможности в полной мере использовать кеш браузера.

webpack предоставил конфигурацию для извлечения этой части кода отдельно для создания файла. Так как эта часть кода часто изменяется, а размер кода очень мал, чтобы уменьшить HTTP-запросы, вы можете встроить эту часть кода в файл шаблона html при упаковке.

В webpack4.x это может быть достигнуто следующей конфигурациейoptimization.runtimeChunk: 'single'. Если вы хотите встроить производственный код среды выполнения в html, вы можете использовать этот плагин для веб-пакетов.inline-manifest-webpack-plugin.

5. Удалите ненужные асинхронные операторы

Синтаксический сахар Async и await может быть хорошим решением проблем асинхронного программирования. Этот синтаксический сахар также можно использовать при написании внешнего кода. Используете ли вы для компиляции кода babel или typescript, на самом деле вы компилируете async и await в генераторы.

Если существует крайняя потребность в размере кода, я не рекомендую использовать async и await во внешнем коде. Поскольку многие сторонние зависимости теперь имеют дело с асинхронностью, используяPromise, зависимости node_modules, которые мы используем, обычно представляют собой скомпилированные исходные файлы ES5, которые являются правильными.PromiseПолифилл готов. И наша собственная конфигурация Babel также будетPromiseСделайте Polyfill, если вы смешаете async и await, babel увеличит связанныеgenerator run timeкод.

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

После добавления асинхронного выражения результат компиляции будет таким, как показано ниже. Можно обнаружить, что в выходном целевом файле есть еще одинgeneraotr runtimeкода, и объем этой части кода все еще относительно велик

Это размер файла перед компиляцией

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

6. Оптимизируйте сторонние зависимости

В первом разделе был кратко представлен метод упаковки для оптимизации сторонних зависимостей. Вот краткое изложение:

  • Если сторонние зависимости поддерживают посткомпиляцию, используют посткомпиляцию и используют сторонние зависимости для загрузки по требованию, не импортируйте полностью библиотеку компонентов;
  • Если сторонняя сторона полагается на компонент, который является относительно большим и редко используется в проекте, а страница загружается по требованию, вы можете выбрать правило конфигурации и упаковать компонент в общедоступный общий доступ только после количества ссылок. превышает во сколько раз, иначе компонент упаковывается прямо в бизнес-код;
  • При введении сторонних зависимостей через теги script и ссылки старайтесь не писать эти ссылки в теге head, вы можете выбрать загрузку и импорт по требованию;

Пост-компиляция, то есть компиляция во время использования, может решить проблему повторной упаковки кода. Импорт по требованию означает, что если кубический пользовательский интерфейс, который я использую, имеет 20 компонентов, но я использую только один из компонентов оповещения, я не импортирую их все и увеличиваю размер выходного файла;

7. lodash вводится по требованию

Библиотека lodash действительно удобна в использовании, но у нее есть недостаток: она имеет большой размер после того, как была полностью импортирована и упакована. Так можно ли ввести лодаш по требованию?

Конечно можно, можно поискать модуль lodash-es на npm, а потом выполнить команду согласно документации экспортировать lodash какes6 modules. Затем вы можете использовать метод импорта для импорта функции отдельно.

На самом деле, как оптимизировать lodash в конце концов, нужно ли оптимизировать, это тоже немного спорно, вы можете прочитать эту статью на Baidu high T grey.Различные попытки оптимизации lodash в webpack. Эта статья Хуиды также демонстрирует сказанное в начале статьи о том, что оптимизация — это компромисс после принятия различных компромиссов в соответствии с потребностями бизнеса.

Резюме важных знаний о веб-пакете

1. Разница между хешем, хэшем контента и хэшем чанка

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

chunkhashЕсли используется вычисление хеш-функции, значение хеш-функции, сгенерированное после каждой сборки, отличается, даже если содержимое файла вообще не изменилось. Таким образом, нет возможности добиться эффекта кэширования, нам нужно изменить другой метод расчета хеш-значения, а именно chunkhash. Chunkhash отличается от хэша тем, что он анализирует зависимые файлы в соответствии с разными входными файлами (Entry), строит соответствующие фрагменты и генерирует соответствующие хеш-значения. Мы отделяем некоторые общедоступные библиотеки и файлы входа в программу в производственной среде, отдельно упаковываем и собираем их, а затем используем хеш-функции для генерации хеш-значений, поэтому, пока мы не меняем код общедоступной библиотеки, мы можем гарантировать, что хеш-значение не будет затронуто.

contenthashПри компиляции кода с помощью webpack мы можем ссылаться на файл css в файле js. Таким образом, два файла должны иметь одно и то же значение хеш-функции. Но есть проблема, если js меняет код, даже если содержимое css файла не изменилось, потому что модуль изменился, css файл будет собираться повторно. В это время мы можем использоватьextra-text-webpack-pluginЗначение contenthash в файле css гарантирует, что даже если содержимое других файлов в модуле, где находится файл css, изменится, пока содержимое файла css остается прежним, построение не будет повторяться.

2. Подробное объяснение splitChunks

Документация webpack4.x, доступная в настоящее время в Интернете, предназначена дляsplitChunksНет полного китайского перевода.Если нет препятствий для чтения на английском языке, вы можете напрямую прочитать официальный документ.Если английский не очень хорош, вы можете обратиться к следующим параметрам и китайской интерпретации:

Во-первых, Webpack4.x автоматически разделит блок кода в соответствии со следующими условиями:

  • Новые блоки кода могут быть общими ссылками или все модули находятся в папке node_modules.
  • Новые блоки кода размером более 30 КБ (раньше min+gziped volume)
  • Блоки кода загружаются по требованию, максимальное количество должно быть меньше или равно 5
  • Первоначально загружаемые кодовые блоки, максимальное количество должно быть меньше или равно 3

// 配置项解释如下
splitChunks: {
    // 默认作用于异步chunk,值为all
    //initial模式下会分开优化打包异步和非异步模块。而all会把异步和非异步同时进行优化打包。也就是说moduleA在indexA中异步引入,indexB中同步引入,initial下moduleA会出现在两个打包块中,而all只会出现一个。
    // all 所有chunk代码(同步加载和异步加载的模块都可以使用)的公共部分分离出来成为一个单独的文件
    // async 将异步加载模块代码公共部分抽离出来一个单独的文件
    chunks: 'async',
    // 默认值是30kb 当文件体积 >= minsize 时将会被拆分为两个文件 某则不生成新的chunk
    minSize: 30000,
    // 共享该module的最小chunk数  (当>= minchunks时才会被拆分为新的chunk)
    minChunks: 1,
    // 最多有5个异步加载请求该module
    maxAsyncRequests: 5,
    // 初始话时最多有3个请求该module
    maxInitialRequests: 3,
    // 名字中间的间隔符
    automaticNameDelimiter: '~',
    // 打包后的名称,如果设置为 truw 默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~ 也可以自己手动指定
    name: true,
    // 设置缓存组用来抽取满足不同规则的chunk, 切割成的每一个新的chunk就是一个cache group
    cacheGroups: {
        common: {
            // 抽取的chunk的名字
            name: 'common',
            // 同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取
            chunks: 'all',
            // 可以为字符串,正则表达式,函数,以module为维度进行抽取,
            // 只要是满足条件的module都会被抽取到该common的chunk中,为函数时第一个参数
            // 是遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组
            test(module, chunks) {
                // module.context 当前文件模块所属的目录 该目录下包含多个文件
                // module.resource 当前模块文件的绝对路径

                if (/scroll/.test(module.context)) {
                    let chunkName = ''; // 引用该chunk的模块名字

                    chunks.forEach(item => {
                        chunkName += item.name + ',';
                    });
                    console.log(`module-scroll`, module.context, chunkName, chunks.length);
                }
            },
            // 优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中, 数值打的优先被选择
            priority: 10,
            // 最少被几个chunk引用
            minChunks: 2,
            //  如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码 (当module未发生变化时是否使用之前的Module)
            reuseExistingChunk: true,
            // 如果cacheGroup中没有设置minSize,则据此判断是否使用上层的minSize,true:则使用0,false:使用上层minSize
            enforce: true
        }
    }
};


Справочная статья