Google – Оптимизация веб-производительности с помощью webpack. Часть 2. Эффективное использование постоянного кэширования

внешний интерфейс JavaScript React.js Webpack
Google – Оптимизация веб-производительности с помощью webpack. Часть 2. Эффективное использование постоянного кэширования

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

Управление версиями пакетов и использование заголовков кеша

Общий способ использования кеша:

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

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

    ⭐️ ПРИМЕЧАНИЕ: Если вы не знакомы сCache-Control, см. статью Джейка Арчибальда:Лучшие практики о кэшировании.

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

    <!-- 修改前 -->
    <script src="./index-v15.js"></script>
    
    <!-- 修改后 -->
    <script src="./index-v16.js"></script>
    
    

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

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

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

HtmlWebpackPluginWebpackManifestPlugin

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

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

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

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

Расширенное чтение

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

полагаться

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

Ключевой термин: В терминологии webpack отдельный файл с кодом приложения называетсяchunk. Мы будем использовать это имя в следующих статьях.

Для извлечения зависимостей в отдельные фрагменты требуются следующие три шага:

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

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

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

  1. БудуentryЗначение объекта изменяется на:

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

    В приведенном выше коде «main» — это имя чанка. Это имя будет использоваться на первом этапе.[name]заменены.

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

  2. в вебпаке 4,могуoptimization.splitChunks.chunks: 'all'Параметры для добавления в конфигурацию веб-пакета:

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

    Эта опция включает интеллектуальное разделение кода. Используя эту функцию, webpack извлечет код сторонней библиотеки размером более 30 КБ (до сжатия и gzip). Он также может извлекать общий код — полезно, если ваша сборка приводит к нескольким пакетам. (Например:Если вы разделите приложение по маршрутизации).

    В вебпаке 3Добавить кCommonsChunkPluginПлагин:

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          // chunk 的名称将会包含依赖
          // 这个名称会在第一步时被 [name] 所替代
          name: 'vendor',
    
          // 这个函数决定哪个模块会被打入 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Только в вебпаке 4). В 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

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

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

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

// index.js
…
…

// 例如,增加这句:
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 упакован, в дополнение к коду модуля, Bundle WebPack также содержитruntime- Небольшой сегмент может управлять кодом, выполняемым модулем. Когда вы разделяете код на несколько файлов, эта небольшая часть кода создает сопоставление между идентификатором фрагмента и соответствующим файлом:

// 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'),
    }),

    // 这个插件必须在 vendor 生成之后执行(因为 webpack 把运行时打进了最新的 chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',

      // minChunks: Infinity 表示任何应用模块都不能打进这个 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>

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

Давайте посмотрим, как это сделать.

Если вы используете HtmlWebpackPlugin для генерации HTML

если вы используетеHtmlWebpackPluginдля создания файлов HTML, вам необходимоInlineSourcePlugin:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      // Inline all files which names start with “runtime~” and end with “.js”.
      // That’s the default naming of runtime chunks
      inlineSource: 'runtime~.+\\.js',
    }),
    // This plugin enables the “inlineSource” option
    new InlineSourcePlugin(),
  ],
};

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

В веб-пакете 4:

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

    // 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(`
        …
        &lt;script>${runtimeContent}&lt;/script>
        …
      `);
    });
    

В веб-пакете 3:

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

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js',
            // → Now the runtime file will be called
            // “runtime.js”, not “runtime.79f17c27b335abc7aaf4.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(`
        …
        &lt;script>${runtimeContent}&lt;/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()функция загрузки.

Это позволяет入口Бандл становится меньше, тем самым сокращается время первой загрузки. Мало того, он еще и кеш может оптимизировать - если изменить код входа CHUNK, то комментарий CHUNK не пострадает.

⭐️ Примечание. Если вы используете Babel для компиляции кода, он не будет распознан Babel.import()Произошла синтаксическая ошибка. Чтобы избежать этой ошибки, вы можете добавитьsyntax-dynamic-importплагин.

Расширенное чтение

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

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

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

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

одностраничное приложение

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

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

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

// 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, комплекты домашней страницы и профиля не будут включать его, и пользователь не загрузит библиотеку при посещении домашней страницы.

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

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

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

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

// webpack.config.js (适用于 webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      // chunk 的名称将会包含公共依赖
      name: 'common',

      // minChunks表示要将一个模块打入公共文件时必须包含的 `minChunks` chunks 数量
      // (注意,插件会分析所有 chunks 和 entries)
      minChunks: 2,    // 2 is the default value
    }),
  ],
};

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

Расширенное чтение

Убедитесь, что идентификатор модуля более стабилен

При создании кода webpack присваивает каждому модулю идентификатор. Впоследствии эти идентификаторы будут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 и т. д.). Но с этим есть проблема, когда вы добавляете модуль, он может оказаться в середине списка модулей, из-за чего ID всех модулей потом будет изменен:

$ 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.jsID изменен с 4 на 5

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

ads.jsID изменен с 5 на 6

   [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Ленивая загрузка некритического кода
  • Разделите код по маршруту или странице, чтобы избежать загрузки ненужных файлов

Чтобы узнать больше, подпишитесь на YFE:

Предыдущий:Перевод] Google - Использование веб-пакета для оптимизации веб-производительности (1): уменьшение размера внешних ресурсов

Следующее сообщение: Google — оптимизация веб-производительности с помощью веб-пакета, часть 3: мониторинг и анализ приложений