Используйте долгосрочное кэширование

Как веб-пакет помогает с кэшированием ресурсов

Следующее (после оптимизации размера приложения , которое сокращает время загрузки приложения) — это кеширование. Используйте его, чтобы хранить части приложения на клиенте и избегать их повторной загрузки каждый раз.

Используйте управление версиями пакетов и заголовки кэша.

Общий подход к кэшированию заключается в следующем:

  1. сообщите браузеру кэшировать файл на очень долгое время (например, год):

    # Server header
    Cache-Control: max-age=31536000
    

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

  2. и переименуйте файл, когда он будет изменен, чтобы принудительно выполнить повторную загрузку:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

Этот подход предписывает браузеру загрузить файл JS, кэшировать его и использовать кэшированную копию. Браузер попадет в сеть только в том случае, если имя файла изменится (или пройдет год).

С вебпаком вы делаете то же самое, но вместо номера версии указываете хеш файла. Чтобы включить хэш в имя файла, используйте [chunkhash] :

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

Если вам нужно имя файла для отправки его клиенту, используйте HtmlWebpackPlugin или WebpackManifestPlugin .

HtmlWebpackPlugin — простой, но менее гибкий подход. Во время компиляции этот плагин генерирует HTML-файл, который включает в себя все скомпилированные ресурсы. Если логика вашего сервера не сложна, то вам этого должно быть достаточно:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin — более гибкий подход, который полезен, если у вас сложная серверная часть. Во время сборки он генерирует файл JSON с сопоставлением имен файлов без хеша и имен файлов с хешем. Используйте этот JSON на сервере, чтобы узнать, с каким файлом работать:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Дальнейшее чтение

Извлечь зависимости и среду выполнения в отдельный файл.

Зависимости

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

Чтобы извлечь зависимости в отдельный чанк, выполните три шага:

  1. Замените имя выходного файла на [name].[chunkname].js :

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    Когда веб-пакет собирает приложение, он заменяет [name] именем чанка. Если мы не добавим часть [name] , нам придется различать чанки по их хешу – а это довольно сложно!

  2. Преобразуйте поле entry в объект:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    В этом фрагменте «main» — это имя чанка. Это имя будет заменено на [name] из шага 1.

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

  3. В веб-пакете 4 добавьте optimization.splitChunks.chunks: 'all' в конфигурацию вашего веб-пакета:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Эта опция включает интеллектуальное разделение кода. С его помощью веб-пакет будет извлекать код поставщика, если его размер превышает 30 КБ (до минификации и gzip). Также будет извлечен общий код — это полезно, если ваша сборка создает несколько пакетов (например , если вы разделите свое приложение на маршруты ).

    В веб-пакет 3 добавьте CommonsChunkPlugin :

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
        // A name of the chunk that will include the dependencies.
        // This name is substituted in place of [name] from step 1
        name: 'vendor',
    
        // A function that determines which modules to include into this chunk
        minChunks: module => module.context && module.context.includes('node_modules'),
        })
      ]
    };
    

    Этот плагин берет все модули, пути которых включают node_modules , и перемещает их в отдельный файл с vendor.[chunkhash].js .

После этих изменений каждая сборка будет генерировать два файла вместо одного: main.[chunkhash].js vendor.[chunkhash].js ( vendors~main.[chunkhash].js для webpack 4). В случае с веб-пакетом 4 пакет поставщика может не сгенерироваться, если зависимости небольшие — и это нормально:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                        Asset      Size  Chunks             Chunk Names
 ./main.00bab6fd3100008a42b0.js   82 kB       0  [emitted]  main
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

Браузер будет кэшировать эти файлы отдельно и повторно загружать только изменяющийся код.

Код времени выполнения веб-пакета

К сожалению, извлечь только код поставщика недостаточно. Если вы попытаетесь изменить что-то в коде приложения:

// index.js
…
…

// E.g. add this:
console.log('Wat');

вы заметите, что хэш vendor также изменится:

                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js  47 kB       1  [emitted]  vendor

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

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack включает эту среду выполнения в последний сгенерированный чанк, который в нашем случае vendor . И каждый раз, когда изменяется какой-либо фрагмент, этот фрагмент кода тоже меняется, вызывая изменение всего фрагмента vendor .

Чтобы решить эту проблему, давайте переместим среду выполнения в отдельный файл. В веб-пакете 4 это достигается включением опции optimization.runtimeChunk :

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

В веб-пакете 3 сделайте это, создав дополнительный пустой чанк с помощью CommonsChunkPlugin :

// webpack.config.js (for webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => module.context && module.context.includes('node_modules')
    }),
    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      // minChunks: Infinity means that no app modules
      // will be included into this chunk
      minChunks: Infinity
    })
  ]
};

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

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main.00bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

Включите их в index.html в обратном порядке — и все готово:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Дальнейшее чтение

Встроенная среда выполнения веб-пакета для сохранения дополнительного HTTP-запроса.

Чтобы сделать ситуацию еще лучше, попробуйте встроить среду выполнения веб-пакета в ответ HTML. То есть вместо этого:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

сделай это:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

Время выполнения невелико, и его встраивание поможет вам сохранить HTTP-запрос (очень важно для HTTP/1; менее важно для HTTP/2, но все равно может иметь эффект).

Вот как это сделать.

Если вы генерируете HTML с помощью HtmlWebpackPlugin

Если вы используете HtmlWebpackPlugin для создания HTML-файла, InlineSourcePlugin — это все, что вам нужно:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

Если вы генерируете HTML с использованием пользовательской логики сервера

С веб-пакетом 4:

  1. Добавьте WebpackManifestPlugin , чтобы узнать сгенерированное имя фрагмента времени выполнения:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    Сборка с этим плагином создаст файл, который выглядит следующим образом:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Встраивайте содержимое фрагмента времени выполнения удобным способом. Например, с Node.js и Express:

    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');
    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Или с веб-пакетом 3:

  1. Сделайте имя среды выполнения статическим, указав filename :

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Встраивайте содержимое runtime.js удобным способом. Например, с Node.js и Express:

    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Код отложенной загрузки, который вам сейчас не нужен

Иногда на странице есть более и менее важные части:

  • Если вы загружаете видео-страницу на YouTube, вас больше волнует видео, чем комментарии. Здесь видео важнее комментариев.
  • Если вы открываете статью на новостном сайте, вас больше волнует текст статьи, чем реклама. Здесь текст важнее рекламы.

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

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

import() указывает, что вы хотите динамически загружать определенный модуль. Когда веб-пакет видит import('./module.js') , он перемещает этот модуль в отдельный фрагмент:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.f7e53d8e13e9a2745d6d.js    60 kB       1  [emitted]  main
 ./vendor.4f14b6326a80f4752a98.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

и загружает его только тогда, когда выполнение достигает функции import() .

Это уменьшит размер main пакета, уменьшив время начальной загрузки. Более того, это улучшит кеширование — если вы измените код в основном фрагменте, это не повлияет на фрагмент комментариев.

Дальнейшее чтение

Разделите код на маршруты и страницы

Если ваше приложение имеет несколько маршрутов или страниц, но есть только один JS-файл с кодом (один main фрагмент), вполне вероятно, что вы обслуживаете дополнительные байты при каждом запросе. Например, когда пользователь посещает домашнюю страницу вашего сайта:

Домашняя страница WebFundamentals

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

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

Для одностраничных приложений

Чтобы разделить одностраничные приложения по маршрутам, используйте import() (см. раздел «Код отложенной загрузки, который вам сейчас не нужен» ). Если вы используете фреймворк, у него может быть существующее решение для этого:

Для традиционных многостраничных приложений

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

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

Для каждого файла записи веб-пакет построит отдельное дерево зависимостей и сгенерирует пакет, включающий только модули, используемые этой записью:

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./home.91b9ed27366fe7e33d6a.js    18 kB       1  [emitted]  home
./article.87a128755b16ac3294fd.js    32 kB       2  [emitted]  article
./profile.de945dc02685f6166781.js    24 kB       3  [emitted]  profile
 ./vendor.4f14b6326a80f4752a98.js    46 kB       4  [emitted]  vendor
./runtime.318d7b8490a7382bf23b.js  1.45 kB       5  [emitted]  runtime

Таким образом, если Lodash используется только на странице статьи, пакеты home и profile не будут включать его — и пользователю не придется загружать эту библиотеку при посещении домашней страницы.

Однако отдельные деревья зависимостей имеют свои недостатки. Если две точки входа используют Lodash, и вы не переместили свои зависимости в пакет поставщика, обе точки входа будут включать копию Lodash. Чтобы решить эту проблему, в веб-пакете 4 добавьте параметр optimization.splitChunks.chunks: 'all' в конфигурацию вашего веб-пакета:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

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

Или в веб-пакете 3 используйте CommonsChunkPlugin — он переместит общие зависимости в новый указанный файл:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

Не стесняйтесь экспериментировать со значением minChunks , чтобы найти лучший вариант. Как правило, вы хотите, чтобы он был небольшим, но увеличивайте его, если количество фрагментов растет. Например, для 3 чанков minChunks может быть 2, а для 30 чанков это может быть 8 — потому что, если вы оставите значение 2, в общий файл попадет слишком много модулей, что приведет к его слишком сильному раздуванию.

Дальнейшее чтение

Сделать идентификаторы модулей более стабильными

При создании кода веб-пакет присваивает каждому модулю идентификатор. Позже эти идентификаторы используются в require() внутри пакета. Обычно вы видите идентификаторы в выводе сборки прямо перед путями к модулям:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.4e50a16675574df6a9e9.js    60 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

↓ Здесь

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

По умолчанию идентификаторы рассчитываются с помощью счетчика (т. е. у первого модуля идентификатор 0, у второго — 1 и т. д.). Проблема в том, что когда вы добавляете новый модуль, он может появиться в середине списка модулей, изменяя идентификаторы всех следующих модулей:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main.0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
   [0] ./index.js 29 kB {1} [built]
   [2] (webpack)/buildin/global.js 488 bytes {2} [built]
   [3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ Мы добавили новый модуль…

[4] ./webPlayer.js 24 kB {1} [built]

↓ И посмотрите, что оно сделало! comments.js теперь имеет идентификатор 5 вместо 4

[5] ./comments.js 58 kB {0} [built]

ads.js теперь имеет идентификатор 6 вместо 5

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

Это делает недействительными все фрагменты, которые включают модули с измененными идентификаторами или зависят от них, даже если их фактический код не изменился. В нашем случае чанк 0 (часть с comments.js ) и main чанк (часть с другим кодом приложения) становятся недействительными, тогда как должен был быть признан только main .

Чтобы решить эту проблему, измените способ расчета идентификаторов модулей с помощью HashedModuleIdsPlugin . Он заменяет идентификаторы на основе счетчиков хэшами путей к модулям:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.6168aaac8461862eab7a.js  22.5 kB       0  [emitted]
   ./main.a2e49a279552980e3b91.js    60 kB       1  [emitted]  main
 ./vendor.ff9f7ea865884e6a84c8.js    46 kB       2  [emitted]  vendor
./runtime.25f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime

↓ Здесь

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

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

Чтобы включить плагин, добавьте его в раздел plugins конфигурации:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Дальнейшее чтение

Подведение итогов

  • Кэшируйте пакет и различайте версии, изменяя имя пакета.
  • Разделите пакет на код приложения, код поставщика и среду выполнения.
  • Встраивание среды выполнения для сохранения HTTP-запроса
  • Отложенная загрузка некритического кода с import
  • Разделите код по маршрутам/страницам, чтобы избежать загрузки ненужного материала.
,

Как веб-пакет помогает с кэшированием ресурсов

Следующее (после оптимизации размера приложения , которое сокращает время загрузки приложения) — это кеширование. Используйте его, чтобы хранить части приложения на клиенте и избегать их повторной загрузки каждый раз.

Используйте управление версиями пакетов и заголовки кэша.

Общий подход к кэшированию заключается в следующем:

  1. сообщите браузеру кэшировать файл на очень долгое время (например, год):

    # Server header
    Cache-Control: max-age=31536000
    

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

  2. и переименуйте файл, когда он будет изменен, чтобы принудительно выполнить повторную загрузку:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

Этот подход предписывает браузеру загрузить файл JS, кэшировать его и использовать кэшированную копию. Браузер попадет в сеть только в том случае, если имя файла изменится (или пройдет год).

С вебпаком вы делаете то же самое, но вместо номера версии указываете хеш файла. Чтобы включить хэш в имя файла, используйте [chunkhash] :

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

Если вам нужно имя файла для отправки его клиенту, используйте HtmlWebpackPlugin или WebpackManifestPlugin .

HtmlWebpackPlugin — простой, но менее гибкий подход. Во время компиляции этот плагин генерирует HTML-файл, который включает в себя все скомпилированные ресурсы. Если логика вашего сервера не сложна, то вам этого должно быть достаточно:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin — более гибкий подход, который полезен, если у вас сложная серверная часть. Во время сборки он генерирует файл JSON с сопоставлением имен файлов без хеша и имен файлов с хешем. Используйте этот JSON на сервере, чтобы узнать, с каким файлом работать:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Дальнейшее чтение

Извлечь зависимости и среду выполнения в отдельный файл.

Зависимости

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

Чтобы извлечь зависимости в отдельный чанк, выполните три шага:

  1. Замените имя выходного файла на [name].[chunkname].js :

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    Когда веб-пакет собирает приложение, он заменяет [name] именем чанка. Если мы не добавим часть [name] , нам придется различать чанки по их хешу – а это довольно сложно!

  2. Преобразуйте поле entry в объект:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    В этом фрагменте «main» — это имя чанка. Это имя будет заменено на [name] из шага 1.

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

  3. В веб-пакете 4 добавьте optimization.splitChunks.chunks: 'all' в конфигурацию вашего веб-пакета:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Эта опция включает интеллектуальное разделение кода. С его помощью веб-пакет будет извлекать код поставщика, если его размер превышает 30 КБ (до минификации и gzip). Также будет извлечен общий код — это полезно, если ваша сборка создает несколько пакетов (например , если вы разделите свое приложение на маршруты ).

    В веб-пакет 3 добавьте CommonsChunkPlugin :

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
        // A name of the chunk that will include the dependencies.
        // This name is substituted in place of [name] from step 1
        name: 'vendor',
    
        // A function that determines which modules to include into this chunk
        minChunks: module => module.context && module.context.includes('node_modules'),
        })
      ]
    };
    

    Этот плагин берет все модули, пути которых включают node_modules , и перемещает их в отдельный файл с vendor.[chunkhash].js .

После этих изменений каждая сборка будет генерировать два файла вместо одного: main.[chunkhash].js vendor.[chunkhash].js ( vendors~main.[chunkhash].js для webpack 4). В случае с веб-пакетом 4 пакет поставщика может не сгенерироваться, если зависимости небольшие — и это нормально:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                        Asset      Size  Chunks             Chunk Names
 ./main.00bab6fd3100008a42b0.js   82 kB       0  [emitted]  main
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

Браузер будет кэшировать эти файлы отдельно и повторно загружать только изменяющийся код.

Код времени выполнения веб-пакета

К сожалению, извлечь только код поставщика недостаточно. Если вы попытаетесь изменить что-то в коде приложения:

// index.js
…
…

// E.g. add this:
console.log('Wat');

вы заметите, что хэш vendor также изменится:

                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js  47 kB       1  [emitted]  vendor

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

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack включает эту среду выполнения в последний сгенерированный чанк, который в нашем случае vendor . И каждый раз, когда изменяется какой-либо фрагмент, этот фрагмент кода тоже меняется, вызывая изменение всего фрагмента vendor .

Чтобы решить эту проблему, давайте переместим среду выполнения в отдельный файл. В веб-пакете 4 это достигается включением опции optimization.runtimeChunk :

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

В веб-пакете 3 сделайте это, создав дополнительный пустой чанк с помощью CommonsChunkPlugin :

// webpack.config.js (for webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => module.context && module.context.includes('node_modules')
    }),
    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      // minChunks: Infinity means that no app modules
      // will be included into this chunk
      minChunks: Infinity
    })
  ]
};

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

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main.00bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

Включите их в index.html в обратном порядке — и все готово:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Дальнейшее чтение

Встроенная среда выполнения веб-пакета для сохранения дополнительного HTTP-запроса.

Чтобы сделать ситуацию еще лучше, попробуйте встроить среду выполнения веб-пакета в ответ HTML. То есть вместо этого:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

сделай это:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

Время выполнения невелико, и его встраивание поможет вам сохранить HTTP-запрос (очень важно для HTTP/1; менее важно для HTTP/2, но все равно может иметь эффект).

Вот как это сделать.

Если вы генерируете HTML с помощью HtmlWebpackPlugin

Если вы используете HtmlWebpackPlugin для создания HTML-файла, InlineSourcePlugin — это все, что вам нужно:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

Если вы генерируете HTML с использованием пользовательской логики сервера

С веб-пакетом 4:

  1. Добавьте WebpackManifestPlugin , чтобы узнать сгенерированное имя фрагмента времени выполнения:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    Сборка с этим плагином создаст файл, который выглядит следующим образом:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Встраивайте содержимое фрагмента времени выполнения удобным способом. Например, с Node.js и Express:

    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');
    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Или с веб-пакетом 3:

  1. Сделайте имя среды выполнения статическим, указав filename :

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Встраивайте содержимое runtime.js удобным способом. Например, с Node.js и Express:

    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Код отложенной загрузки, который вам сейчас не нужен

Иногда на странице есть более и менее важные части:

  • Если вы загружаете видео-страницу на YouTube, вас больше волнует видео, чем комментарии. Здесь видео важнее комментариев.
  • Если вы открываете статью на новостном сайте, вас больше волнует текст статьи, чем реклама. Здесь текст важнее рекламы.

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

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

import() указывает, что вы хотите динамически загружать определенный модуль. Когда веб-пакет видит import('./module.js') , он перемещает этот модуль в отдельный фрагмент:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.f7e53d8e13e9a2745d6d.js    60 kB       1  [emitted]  main
 ./vendor.4f14b6326a80f4752a98.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

и загружает его только тогда, когда выполнение достигает функции import() .

Это уменьшит размер main пакета, уменьшив время начальной загрузки. Более того, это улучшит кеширование — если вы измените код в основном фрагменте, это не повлияет на фрагмент комментариев.

Дальнейшее чтение

Разделите код на маршруты и страницы

Если ваше приложение имеет несколько маршрутов или страниц, но есть только один JS-файл с кодом (один main фрагмент), вполне вероятно, что вы обслуживаете дополнительные байты при каждом запросе. Например, когда пользователь посещает домашнюю страницу вашего сайта:

Домашняя страница WebFundamentals

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

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

Для одностраничных приложений

Чтобы разделить одностраничные приложения по маршрутам, используйте import() (см. раздел «Код отложенной загрузки, который вам сейчас не нужен» ). Если вы используете фреймворк, у него может быть существующее решение для этого:

Для традиционных многостраничных приложений

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

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

Для каждого файла записи веб-пакет построит отдельное дерево зависимостей и сгенерирует пакет, включающий только модули, используемые этой записью:

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./home.91b9ed27366fe7e33d6a.js    18 kB       1  [emitted]  home
./article.87a128755b16ac3294fd.js    32 kB       2  [emitted]  article
./profile.de945dc02685f6166781.js    24 kB       3  [emitted]  profile
 ./vendor.4f14b6326a80f4752a98.js    46 kB       4  [emitted]  vendor
./runtime.318d7b8490a7382bf23b.js  1.45 kB       5  [emitted]  runtime

Таким образом, если Lodash используется только на странице статьи, пакеты home и profile не будут включать его — и пользователю не придется загружать эту библиотеку при посещении домашней страницы.

Однако отдельные деревья зависимостей имеют свои недостатки. Если две точки входа используют Lodash, и вы не переместили свои зависимости в пакет поставщика, обе точки входа будут включать копию Lodash. Чтобы решить эту проблему, в веб-пакете 4 добавьте параметр optimization.splitChunks.chunks: 'all' в конфигурацию вашего веб-пакета:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

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

Или в веб-пакете 3 используйте CommonsChunkPlugin — он переместит общие зависимости в новый указанный файл:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

Не стесняйтесь экспериментировать со значением minChunks , чтобы найти лучший вариант. Как правило, вы хотите, чтобы он был небольшим, но увеличивайте его, если количество фрагментов растет. Например, для 3 чанков minChunks может быть 2, а для 30 чанков это может быть 8 — потому что, если вы оставите значение 2, в общий файл попадет слишком много модулей, что слишком сильно его раздует.

Дальнейшее чтение

Сделать идентификаторы модулей более стабильными

При создании кода веб-пакет присваивает каждому модулю идентификатор. Позже эти идентификаторы используются в require() внутри пакета. Обычно вы видите идентификаторы в выводе сборки прямо перед путями к модулям:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.4e50a16675574df6a9e9.js    60 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

↓ Здесь

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

По умолчанию идентификаторы рассчитываются с помощью счетчика (т. е. у первого модуля идентификатор 0, у второго — 1 и т. д.). Проблема в том, что когда вы добавляете новый модуль, он может появиться в середине списка модулей, изменяя идентификаторы всех следующих модулей:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main.0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
   [0] ./index.js 29 kB {1} [built]
   [2] (webpack)/buildin/global.js 488 bytes {2} [built]
   [3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ Мы добавили новый модуль…

[4] ./webPlayer.js 24 kB {1} [built]

↓ И посмотрите, что оно сделало! comments.js теперь имеет идентификатор 5 вместо 4

[5] ./comments.js 58 kB {0} [built]

ads.js теперь имеет идентификатор 6 вместо 5

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

Это делает недействительными все фрагменты, которые включают модули с измененными идентификаторами или зависят от них, даже если их фактический код не изменился. В нашем случае чанк 0 (чанк с comments.js ) и main чанк (чанк с другим кодом приложения) становятся недействительными, тогда как должен был быть признан только main .

Чтобы решить эту проблему, измените способ расчета идентификаторов модулей с помощью HashedModuleIdsPlugin . Он заменяет идентификаторы на основе счетчиков хэшами путей к модулям:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.6168aaac8461862eab7a.js  22.5 kB       0  [emitted]
   ./main.a2e49a279552980e3b91.js    60 kB       1  [emitted]  main
 ./vendor.ff9f7ea865884e6a84c8.js    46 kB       2  [emitted]  vendor
./runtime.25f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime

↓ Здесь

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

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

Чтобы включить плагин, добавьте его в раздел plugins конфигурации:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Дальнейшее чтение

Подведение итогов

  • Кэшируйте пакет и различайте версии, изменяя имя пакета.
  • Разделите пакет на код приложения, код поставщика и среду выполнения.
  • Встраивание среды выполнения для сохранения HTTP-запроса
  • Отложенная загрузка некритического кода с import
  • Разделите код по маршрутам/страницам, чтобы избежать загрузки ненужного материала.
,

Как веб-пакет помогает с кэшированием ресурсов

Следующее (после оптимизации размера приложения , которое сокращает время загрузки приложения) — это кеширование. Используйте его, чтобы хранить части приложения на клиенте и избегать их повторной загрузки каждый раз.

Используйте управление версиями пакетов и заголовки кэша.

Общий подход к кэшированию заключается в следующем:

  1. сообщите браузеру кэшировать файл на очень долгое время (например, год):

    # Server header
    Cache-Control: max-age=31536000
    

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

  2. и переименуйте файл, когда он будет изменен, чтобы принудительно выполнить повторную загрузку:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

Этот подход предписывает браузеру загрузить файл JS, кэшировать его и использовать кэшированную копию. Браузер попадет в сеть только в том случае, если имя файла изменится (или пройдет год).

С вебпаком вы делаете то же самое, но вместо номера версии указываете хеш файла. Чтобы включить хэш в имя файла, используйте [chunkhash] :

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

Если вам нужно имя файла для отправки его клиенту, используйте HtmlWebpackPlugin или WebpackManifestPlugin .

HtmlWebpackPlugin — простой, но менее гибкий подход. Во время компиляции этот плагин генерирует HTML-файл, который включает в себя все скомпилированные ресурсы. Если логика вашего сервера не сложна, то вам этого должно быть достаточно:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin — более гибкий подход, который полезен, если у вас сложная серверная часть. Во время сборки он генерирует файл JSON с сопоставлением имен файлов без хеша и имен файлов с хешем. Используйте этот JSON на сервере, чтобы узнать, с каким файлом работать:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Дальнейшее чтение

Извлечь зависимости и среду выполнения в отдельный файл.

Зависимости

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

Чтобы извлечь зависимости в отдельный чанк, выполните три шага:

  1. Замените имя выходного файла на [name].[chunkname].js :

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    Когда веб-пакет собирает приложение, он заменяет [name] именем чанка. Если мы не добавим часть [name] , нам придется различать чанки по их хешу – а это довольно сложно!

  2. Преобразуйте поле entry в объект:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    В этом фрагменте «main» — это имя чанка. Это имя будет заменено на [name] из шага 1.

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

  3. В веб-пакете 4 добавьте optimization.splitChunks.chunks: 'all' в конфигурацию вашего веб-пакета:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Эта опция включает интеллектуальное разделение кода. С его помощью веб-пакет будет извлекать код поставщика, если его размер превышает 30 КБ (до минификации и gzip). Также будет извлечен общий код — это полезно, если ваша сборка создает несколько пакетов (например , если вы разделите свое приложение на маршруты ).

    В веб-пакет 3 добавьте CommonsChunkPlugin :

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
        // A name of the chunk that will include the dependencies.
        // This name is substituted in place of [name] from step 1
        name: 'vendor',
    
        // A function that determines which modules to include into this chunk
        minChunks: module => module.context && module.context.includes('node_modules'),
        })
      ]
    };
    

    Этот плагин берет все модули, пути которых включают node_modules , и перемещает их в отдельный файл с vendor.[chunkhash].js .

После этих изменений каждая сборка будет генерировать два файла вместо одного: main.[chunkhash].js vendor.[chunkhash].js ( vendors~main.[chunkhash].js для webpack 4). В случае с веб-пакетом 4 пакет поставщика может не сгенерироваться, если зависимости небольшие — и это нормально:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                        Asset      Size  Chunks             Chunk Names
 ./main.00bab6fd3100008a42b0.js   82 kB       0  [emitted]  main
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

Браузер будет кэшировать эти файлы отдельно и повторно загружать только изменяющийся код.

Код времени выполнения веб-пакета

К сожалению, извлечь только код поставщика недостаточно. Если вы попытаетесь что-то изменить в коде приложения:

// index.js
…
…

// E.g. add this:
console.log('Wat');

вы заметите, что хэш vendor также изменится:

                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js  47 kB       1  [emitted]  vendor

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

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack включает эту среду выполнения в последний сгенерированный чанк, который в нашем случае vendor . И каждый раз, когда меняется какой-либо фрагмент, этот фрагмент кода тоже меняется, вызывая изменение всего фрагмента vendor .

Чтобы решить эту проблему, давайте переместим среду выполнения в отдельный файл. В веб-пакете 4 это достигается включением опции optimization.runtimeChunk :

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

В веб-пакете 3 сделайте это, создав дополнительный пустой чанк с помощью CommonsChunkPlugin :

// webpack.config.js (for webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => module.context && module.context.includes('node_modules')
    }),
    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      // minChunks: Infinity means that no app modules
      // will be included into this chunk
      minChunks: Infinity
    })
  ]
};

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

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main.00bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

Включите их в index.html в обратном порядке — и все готово:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Дальнейшее чтение

Встроенная среда выполнения веб-пакета для сохранения дополнительного HTTP-запроса.

Чтобы сделать ситуацию еще лучше, попробуйте встроить среду выполнения веб-пакета в ответ HTML. То есть вместо этого:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

сделай это:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

Время выполнения невелико, и его встраивание поможет вам сохранить HTTP-запрос (очень важно для HTTP/1; менее важно для HTTP/2, но все равно может иметь эффект).

Вот как это сделать.

Если вы генерируете HTML с помощью HtmlWebpackPlugin

Если вы используете HtmlWebpackPlugin для создания HTML-файла, InlineSourcePlugin — это все, что вам нужно:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

Если вы генерируете HTML с использованием пользовательской логики сервера

С веб-пакетом 4:

  1. Добавьте WebpackManifestPlugin , чтобы узнать сгенерированное имя фрагмента времени выполнения:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    Сборка с этим плагином создаст файл, который выглядит следующим образом:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Встраивайте содержимое фрагмента времени выполнения удобным способом. Например, с Node.js и Express:

    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');
    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Или с веб-пакетом 3:

  1. Сделайте имя среды выполнения статическим, указав filename :

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Встраивайте содержимое runtime.js удобным способом. Например, с Node.js и Express:

    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Код отложенной загрузки, который вам сейчас не нужен

Иногда на странице есть более и менее важные части:

  • Если вы загружаете видео-страницу на YouTube, вас больше волнует видео, чем комментарии. Здесь видео важнее комментариев.
  • Если вы открываете статью на новостном сайте, вас больше волнует текст статьи, чем реклама. Здесь текст важнее рекламы.

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

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

import() указывает, что вы хотите динамически загружать определенный модуль. Когда веб-пакет видит import('./module.js') , он перемещает этот модуль в отдельный фрагмент:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.f7e53d8e13e9a2745d6d.js    60 kB       1  [emitted]  main
 ./vendor.4f14b6326a80f4752a98.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

и загружает его только тогда, когда выполнение достигает функции import() .

Это уменьшит main пакет, уменьшив время начальной загрузки. Более того, это улучшит кеширование — если вы измените код в основном блоке, блок комментариев не пострадает.

Дальнейшее чтение

Разделите код на маршруты и страницы

Если ваше приложение имеет несколько маршрутов или страниц, но есть только один JS-файл с кодом (один main фрагмент), вполне вероятно, что вы обслуживаете дополнительные байты при каждом запросе. Например, когда пользователь посещает домашнюю страницу вашего сайта:

Домашняя страница WebFundamentals

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

Если мы разделим приложение на страницы (или маршруты, если это одностраничное приложение), пользователь загрузит только соответствующий код. Кроме того, браузер будет лучше кэшировать код приложения: если вы измените код домашней страницы, WebPack будет аннулировать только соответствующий кусок.

Для одностраничных приложений

Чтобы разделить одностраничные приложения по маршрутам, используйте import() (см. «Код« ленивый код, который вам не нужен прямо сейчас » ). Если вы используете структуру, у него может быть существующее решение для этого:

Для традиционных многостраничных приложений

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

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

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

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./home.91b9ed27366fe7e33d6a.js    18 kB       1  [emitted]  home
./article.87a128755b16ac3294fd.js    32 kB       2  [emitted]  article
./profile.de945dc02685f6166781.js    24 kB       3  [emitted]  profile
 ./vendor.4f14b6326a80f4752a98.js    46 kB       4  [emitted]  vendor
./runtime.318d7b8490a7382bf23b.js  1.45 kB       5  [emitted]  runtime

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

Отдельные деревья зависимости имеют свои недостатки. Если две точки входа используют Lodash, и вы не переместили свои зависимости в пакет продавцов, обе точки входа будут включать копию Lodash. Чтобы решить это, в WebPack 4 добавьте опцию optimization.splitChunks.chunks: 'all' в вариант в вашем конфигурации WebPack:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

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

Или, в WebPack 3, используйте CommonsChunkPlugin - он перенесет общие зависимости в новый указанный файл:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

Не стесняйтесь играть с значением minChunks чтобы найти лучшего. Как правило, вы хотите, чтобы он был маленьким, но увеличивается, если увеличивается количество кусков. Например, для 3 кусков minChunks может быть 2, но для 30 кусков это может быть 8 - потому что, если вы сохраните его на 2, слишком много модулей попадут в общий файл, надув его слишком много.

Дальнейшее чтение

Сделайте идентификаторы модуля более стабильными

При создании кода WebPack назначает каждому модулю идентификатор. Позже эти идентификаторы используются в require() s внутри пакета. Вы обычно видите идентификаторы на выходе сборки прямо перед путями модуля:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.4e50a16675574df6a9e9.js    60 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

↓ Здесь

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

По умолчанию идентификаторы рассчитываются с использованием счетчика (то есть у первого модуля есть ID 0, второй имеет ID 1 и т. Д.). Проблема в том, что когда вы добавляете новый модуль, он может появиться в середине списка модулей, изменяя все идентификаторы всех следующих модулей:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main.0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
   [0] ./index.js 29 kB {1} [built]
   [2] (webpack)/buildin/global.js 488 bytes {2} [built]
   [3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ Мы добавили новый модуль…

[4] ./webPlayer.js 24 kB {1} [built]

↓ и посмотрите, что он сделал! comments.js теперь имеет id 5 вместо 4

[5] ./comments.js 58 kB {0} [built]

ads.js теперь имеет ID 6 вместо 5

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

Это лишает недействительных всех кусков, которые включают или зависят от модулей с измененными идентификаторами - даже если их фактический код не изменился. В нашем случае 0 кусок (кусок с comments.js ) и main кусок (кусок с другим кодом приложения) получают недействительную - тогда как только main должен был быть.

Чтобы решить это, измените, как идентификаторы модулей рассчитываются с использованием HashedModuleIdsPlugin . Он заменяет идентификаторы на основе контр, хешами модульных путей:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.6168aaac8461862eab7a.js  22.5 kB       0  [emitted]
   ./main.a2e49a279552980e3b91.js    60 kB       1  [emitted]  main
 ./vendor.ff9f7ea865884e6a84c8.js    46 kB       2  [emitted]  vendor
./runtime.25f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime

↓ Здесь

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

С этим подходом идентификатор модуля меняется только в случае переименования или перемещения этого модуля. Новые модули не будут влиять на идентификаторы других модулей.

Чтобы включить плагин, добавьте его в раздел plugins конфигурации:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Дальнейшее чтение

Суммирование

  • Кэшируйте пакет и дифференцируйте версии, изменяя имя пакета
  • Разделите пакет на код приложения, код поставщика и время выполнения.
  • Встроить время выполнения, чтобы сохранить HTTP -запрос
  • Некритический код с ленивой нагрузкой с import
  • Разделенный код по маршрутам/страницам, чтобы избежать загрузки ненужных вещей
,

Как WebPack помогает с кэшированием активов

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

Используйте версии пакета и заголовки кеша

Общий подход к кэшированию - это:

  1. Сообщите браузеру кэшировать файл в течение очень долгого времени (например, год):

    # Server header
    Cache-Control: max-age=31536000
    

    Если вы не знакомы, что делает Cache-Control , см. Отличный пост Джейка Арчибальда о лучших практиках кэширования .

  2. и переименовать файл при изменении, чтобы заставить повторную загрузку:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

Этот подход сообщает браузеру загрузить файл JS, кэшировать его и использовать кэшированную копию. Браузер попадет в сеть только в том случае, если имя файла изменится (или если пройдет год).

С Webpack вы делаете то же самое, но вместо номера версии вы указываете хэш файла. Чтобы включить хэш в имя файла, используйте [chunkhash] :

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

Если вам нужно имя файла, чтобы отправить его клиенту, используйте HtmlWebpackPlugin или WebpackManifestPlugin .

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

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin - более гибкий подход, который полезен, если у вас есть сложная часть сервера. Во время сборки он генерирует файл JSON с отображением между именами файлов без хэш и имен файлов с хэшем. Используйте этот JSON на сервере, чтобы узнать, с каким файлом работать:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Дальнейшее чтение

Извлеките зависимости и время выполнения в отдельный файл

Зависимости

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

Чтобы извлечь зависимости в отдельный кусок, выполните три шага:

  1. Замените выходное имя файла на [name].[chunkname].js :

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    Когда WebPack создает приложение, оно заменяет [name] с именем куски. Если мы не добавим часть [name] , нам придется различать кусочки по их хэш - что довольно сложно!

  2. Преобразовать поле entry в объект:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    В этом фрагменте «Main» - это имя кусочка. Это имя будет заменено вместо [name] с шага 1.

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

  3. В WebPack 4 добавьте опцию optimization.splitChunks.chunks: 'all' в конфигурации WebPack:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Эта опция включает в себя смарт -код расщепление. С помощью этого WebPack извлекает код поставщика, если он будет превышать 30 КБ (перед министерством и GZIP). Это также извлекла бы общий код - это полезно, если ваша сборка производит несколько пакетов (например , если вы разделяете свое приложение на маршруты ).

    В Webpack 3 добавьте CommonsChunkPlugin :

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
        // A name of the chunk that will include the dependencies.
        // This name is substituted in place of [name] from step 1
        name: 'vendor',
    
        // A function that determines which modules to include into this chunk
        minChunks: module => module.context && module.context.includes('node_modules'),
        })
      ]
    };
    

    Этот плагин берет все модули, которые пути включают в себя node_modules и перемещает их в отдельный файл, называемый vendor.[chunkhash].js .

После этих изменений каждая сборка будет генерировать два файла вместо одного: main.[chunkhash].js vendors~main.[chunkhash].js vendor.[chunkhash].js В случае WebPack 4, пакет продавцов может не генерироваться, если зависимости невелики - и это нормально:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                        Asset      Size  Chunks             Chunk Names
 ./main.00bab6fd3100008a42b0.js   82 kB       0  [emitted]  main
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

Браузер будет кэшировать эти файлы отдельно - и Redownload только код, который меняется.

WebPack Code

К сожалению, извлечения только кода поставщика недостаточно. Если вы попытаетесь что -то изменить в коде приложения:

// index.js
…
…

// E.g. add this:
console.log('Wat');

Вы заметите, что хэш vendor также меняется:

                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js  47 kB       1  [emitted]  vendor

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

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

WebPack включает в себя это время выполнения в последнюю сгенерированную кусок, который является vendor в нашем случае. И каждый раз, когда любой кусок меняется, этот кусок кода тоже меняется, что приводит к изменению всей vendor .

Чтобы решить это, давайте перенесем время выполнения в отдельный файл. В WebPack 4 это достигается путем включения опции optimization.runtimeChunk :

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

В WebPack 3 сделайте это, создав дополнительный пустой кусок с CommonsChunkPlugin :

// webpack.config.js (for webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => module.context && module.context.includes('node_modules')
    }),
    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      // minChunks: Infinity means that no app modules
      // will be included into this chunk
      minChunks: Infinity
    })
  ]
};

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

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main.00bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

Включите их в index.html в обратном порядке - и все готово:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Дальнейшее чтение

Встроенное время выполнения WebPack для сохранения дополнительного HTTP -запроса

Чтобы сделать вещи еще лучше, попробуйте внедрить время выполнения WebPack в ответ HTML. Т.е. вместо этого:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

Сделай это:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

Среда выполнения невелика, и внедрение оно поможет вам сохранить HTTP -запрос (довольно важно для HTTP/1; менее важен с HTTP/2, но все еще может воспроизводить эффект).

Вот как это сделать.

Если вы генерируете HTML с HTMlWebpackplugin

Если вы используете htmlwebpackplugin для генерации HTML -файла, это все, что вам нужно: Inlinesourceplugin :

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

Если вы генерируете HTML с помощью пользовательской логики сервера

С Webpack 4:

  1. Добавьте WebpackManifestPlugin чтобы узнать сгенерированное имя чанка времени выполнения:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    Сборка с этим плагином создаст файл, который выглядит так:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Встройте содержание чанка времени выполнения удобно. Например, с node.js и Express:

    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');
    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Или с WebPack 3:

  1. Сделайте имя во время выполнения статичным, указав filename :

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Встройте контент runtime.js удобно. Например, с node.js и Express:

    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Код с ленивой нагрузкой, который вам сейчас не нужен

Иногда на странице есть более и менее важные части:

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

В таких случаях улучшите начальную производительность загрузки, сначала загрузив только самые важные вещи, а позже загрузите оставшиеся детали. Используйте функцию import() и распределение кода для этого:

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

import() указывает, что вы хотите динамически загрузить определенный модуль. Когда WebPack видит import('./module.js') , он перемещает этот модуль в отдельный кусок:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.f7e53d8e13e9a2745d6d.js    60 kB       1  [emitted]  main
 ./vendor.4f14b6326a80f4752a98.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

и загружает его только тогда, когда выполнение достигает функции import() .

Это сделает main пакет меньше, улучшая начальное время загрузки. Более того, это улучшит кэширование - если вы измените код в основной части, комментарии Chunk не повлияет.

Дальнейшее чтение

Разделите код на маршруты и страницы

Если в вашем приложении есть несколько маршрутов или страниц, но есть только один файл JS с кодом (один main кусок), вполне вероятно, что вы обслуживаете дополнительные байты по каждому запросу. Например, когда пользователь посещает домашнюю страницу вашего сайта:

Домашняя страница веб -сайта

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

Если мы разделим приложение на страницы (или маршруты, если это приложение для одной страницы), пользователь загрузит только соответствующий код. Кроме того, браузер будет лучше кэшировать код приложения: если вы измените код домашней страницы, WebPack будет аннулировать только соответствующий кусок.

Для одностраничных приложений

Чтобы разделить одностраничные приложения по маршрутам, используйте import() (см. «Код« ленивый код, который вам не нужен прямо сейчас » ). Если вы используете структуру, у него может быть существующее решение для этого:

Для традиционных многостраничных приложений

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

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

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

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./home.91b9ed27366fe7e33d6a.js    18 kB       1  [emitted]  home
./article.87a128755b16ac3294fd.js    32 kB       2  [emitted]  article
./profile.de945dc02685f6166781.js    24 kB       3  [emitted]  profile
 ./vendor.4f14b6326a80f4752a98.js    46 kB       4  [emitted]  vendor
./runtime.318d7b8490a7382bf23b.js  1.45 kB       5  [emitted]  runtime

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

Отдельные деревья зависимости имеют свои недостатки. Если две точки входа используют Lodash, и вы не переместили свои зависимости в пакет продавцов, обе точки входа будут включать копию Lodash. Чтобы решить это, в WebPack 4 добавьте опцию optimization.splitChunks.chunks: 'all' в вариант в вашем конфигурации WebPack:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

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

Или, в WebPack 3, используйте CommonsChunkPlugin - он перенесет общие зависимости в новый указанный файл:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

Не стесняйтесь играть с значением minChunks чтобы найти лучшего. Как правило, вы хотите, чтобы он был маленьким, но увеличивается, если увеличивается количество кусков. Например, для 3 кусков minChunks может быть 2, но для 30 кусков это может быть 8 - потому что, если вы сохраните его на 2, слишком много модулей попадут в общий файл, надув его слишком много.

Дальнейшее чтение

Сделайте идентификаторы модуля более стабильными

При создании кода WebPack назначает каждому модулю идентификатор. Позже эти идентификаторы используются в require() s внутри пакета. Вы обычно видите идентификаторы на выходе сборки прямо перед путями модуля:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.4e50a16675574df6a9e9.js    60 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

↓ Здесь

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

По умолчанию идентификаторы рассчитываются с использованием счетчика (то есть у первого модуля есть ID 0, второй имеет ID 1 и т. Д.). Проблема в том, что когда вы добавляете новый модуль, он может появиться в середине списка модулей, изменяя все идентификаторы всех следующих модулей:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main.0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
   [0] ./index.js 29 kB {1} [built]
   [2] (webpack)/buildin/global.js 488 bytes {2} [built]
   [3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ Мы добавили новый модуль…

[4] ./webPlayer.js 24 kB {1} [built]

↓ и посмотрите, что он сделал! comments.js теперь имеет id 5 вместо 4

[5] ./comments.js 58 kB {0} [built]

ads.js теперь имеет ID 6 вместо 5

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

Это лишает недействительных всех кусков, которые включают или зависят от модулей с измененными идентификаторами - даже если их фактический код не изменился. В нашем случае 0 кусок (кусок с comments.js ) и main кусок (кусок с другим кодом приложения) получают недействительную - тогда как только main должен был быть.

Чтобы решить это, измените, как идентификаторы модулей рассчитываются с использованием HashedModuleIdsPlugin . Он заменяет идентификаторы на основе контр, хешами модульных путей:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.6168aaac8461862eab7a.js  22.5 kB       0  [emitted]
   ./main.a2e49a279552980e3b91.js    60 kB       1  [emitted]  main
 ./vendor.ff9f7ea865884e6a84c8.js    46 kB       2  [emitted]  vendor
./runtime.25f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime

↓ Здесь

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

С этим подходом идентификатор модуля меняется только в случае переименования или перемещения этого модуля. Новые модули не будут влиять на идентификаторы других модулей.

Чтобы включить плагин, добавьте его в раздел plugins конфигурации:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Дальнейшее чтение

Суммирование

  • Кэшируйте пакет и дифференцируйте версии, изменяя имя пакета
  • Разделите пакет на код приложения, код поставщика и время выполнения.
  • Встроить время выполнения, чтобы сохранить HTTP -запрос
  • Некритический код с ленивой нагрузкой с import
  • Разделенный код по маршрутам/страницам, чтобы избежать загрузки ненужных вещей