Хабрахабр

[Перевод] Поиск правильного способа разделения материалов сайтов с помощью Webpack

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

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

image

Общие сведения

В соответствии с глоссарием Webpack, существуют две стратегии разделения файлов. Это — разделение бандла (bundle splitting) и разделение кода (code splitting). Эти термины могут показаться взаимозаменяемыми, но таковыми они не являются.

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

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

И, пожалуй, от её внедрения способны выиграть буквально все веб-проекты. Однако мне хотелось бы сказать, что для многих сайтов гораздо более ценным оказывается именно первая стратегия — разделение бандлов.

Поговорим об этом подробнее.

Разделение бандлов

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

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

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

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

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

Вот сценарий, подходящий под общее описание, данное в предыдущем абзаце:

  • Алиса посещает наш сайт раз в неделю в течение 10 недель.
  • Мы обновляем сайт один раз в неделю.
  • Мы, каждую неделю, обновляем страницу со списком товаров (product list).
  • Кроме того, у нас есть страница с подробными сведениями о товаре (product details), но мы пока над ней не работаем.
  • На пятой неделе мы добавляем в материалы проекта новый npm-пакет.
  • На восьмой неделе мы обновляем один из уже используемых в проекте npm-пакетов.

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

▍Исходные условия

Предположим, общий размер нашего JavaScript-пакета составляет немалых 400 Кб и мы, в текущих условиях, передаём всё это пользователю в виде одного файла main.js. У нас имеется конфигурация Webpack, которая, в общих чертах, подобна нижеприведённой (то, что к нашему разговору не относится, я оттуда убрал):

const path = require('path'); module.exports = ,
};

Webpack называет результирующий файл main.js в том случае, когда в конфигурации имеется единственная запись entry.

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

В результате, посещая еженедельно наш сайт, Алиса вынуждена загружать новый файл размером 400 Кб. В соответствии с вышеописанным сценарием, когда мы, каждую неделю, вносим в код сайта какие-то изменения, строка contenthash пакета меняется.

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

Объём данных, загруженных пользователем

12 Мб кода. В результате оказывается, что пользователь, за 10 недель, загрузил 4. Этот показатель можно улучшить.

▍Отделение пакетов сторонних разработчиков от основного кода

Разделим большой пакет на две части. Наш собственный код будет в файле main.js, а код сторонних разработчиков в файле vendor.js. Сделать это несложно, в этом нам поможет следующая конфигурация Webpack:

const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', }, optimization: { splitChunks: { chunks: 'all', }, },
};

Webpack 4 старается максимально облегчить жизнь разработчику, поэтому он делает всё, что может, и при этом не требует, чтобы ему сообщали о том, как именно нужно разбивать бандлы на части.

Такое вот автоматическое поведение программы приводит к немногочисленным восторгам, вроде: «Ну что за прелесть этот Webpack», и к множеству вопросов в духе: «А что это тут делается с моими бандлами?».

В любом случае, добавление в конфигурацию конструкции optimization.splitChunks.chunks = 'all' сообщает Webpack о том, что нам надо, чтобы он взял всё из node_modules и поместил бы это в файл vendors~main.js.

А вот файл vendor.js она загрузит лишь три раза. После того, как мы провели такое вот базовое разделение бандла, Алиса, регулярно посещающая наш сайт еженедельно, будет загружать при каждом визите файл main.js размером 200 Кб. Вот соответствующая таблица, в которой, волею судьбы, размеры файлов main.js и vendor.js в первые четыре недели совпадают и равняются 200 Кб. Произойдёт это во время визитов в первую, пятую и восьмую недели.

Объём данных, загруженных пользователем

64 Мб. В результате получается, что объём загруженных пользователем за 10 недель данных составил 2. Не такой уж и плохой результат, достигнутый добавлением нескольких строк в конфигурационный файл. То есть, в сравнении с тем, что было до разделения бандла, объём уменьшился на 36%. А если вам надо обновиться с Webpack 3 на 4 — делайте это и не беспокойтесь, так как процесс это довольно простой и всё ещё бесплатный. Кстати, прежде чем читать дальше — сделайте то же самое в своём проекте.

Однако если считать объём данных, отправленных лояльному пользователю, то это честное сокращение этого объёма на 36%. Мне кажется, что рассматриваемое тут улучшение выглядит несколько абстрактно, так как оно растянуто на 10 недель. Это очень хороший результат, но его можно улучшить.

▍Выделение пакетов в отдельные файлы

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

Сделать это совсем несложно, поэтому давайте разложим наши react, lodash, redux, moment, и прочее подобное, по отдельным файлам. Почему бы нам не сформировать самостоятельные файлы для каждого npm-пакета? В этом нам поможет следующая конфигурация Webpack:

const path = require('path');
const webpack = require('webpack'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), plugins: [ new webpack.HashedModuleIdsPlugin(), // в результате хэши не будут неожиданно меняться ], output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', }, optimization: { runtimeChunk: 'single', splitChunks: { chunks: 'all', maxInitialRequests: Infinity, minSize: 0, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name(module) { // получает имя, то есть node_modules/packageName/not/this/part.js // или node_modules/packageName const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]; // имена npm-пакетов можно, не опасаясь проблем, использовать // в URL, но некоторые серверы не любят символы наподобие @ return `npm.${packageName.replace('@', '')}`; }, }, }, }, },
};

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

  • В Webpack есть вполне разумные стандартные установки, которые, на деле, оказываются не такими уж и разумными. Например, максимальное количество выходных файлов установлено в значение 3, минимальный размер файла — в 30 Кб (то есть, файлы меньшего размера будут объединяться). Я это переопределил.
  • cacheGroups — это место, где мы задаём правила того, как Webpack должен сгруппировать данные в выходных файлах. У меня тут есть одна группа, vendor, которая будет использоваться для любого модуля, загруженного из node_modules. Обычно имя (name) для выходного файла задают в виде строки. Но я задал name в виде функции, которая будет вызываться для каждого обработанного файла. Затем я беру имя пакета из пути к модулю. В результате у нас получается один файл для каждого пакета. Например, npm.react-dom.899sadfhj4.js.
  • Имена пакетов, для того, чтобы их можно было опубликовать в npm, должны подходить для использования их в URL, поэтому нам не нужно выполнять операцию encodeURI для имён packageName. Однако я столкнулся с проблемой, которая заключается в том, что .NET-сервер отказывается работать с файлами, в именах которых есть символ @ (такие имена используются для пакетов с заданной областью действия имени, так называемых scoped packages), поэтому я, в соответствующем фрагменте кода, от подобных символов избавляюсь.

Вышеприведённая конфигурация Webpack хороша тем, что её можно один раз настроить, а потом о ней забыть. Она не требует обращения к конкретным пакетам по именам, поэтому, после её создания, она, даже при изменении состава пакетов, остаётся актуальной.

Алиса, наш постоянный посетитель, всё ещё каждую неделю заново загружает 200-килобайтный main.js, а при первом посещении сайта вынуждена загружать 200 Кб npm-пакетов, однако ей не придётся дважды загружать одни и те же пакеты.

По странному стечению обстоятельств размер каждого файла с npm-пакетами составляет 20 Кб. Ниже показана новая версия таблицы со сведениями об объёмах еженедельных загрузок данных.

Объём данных, загруженных пользователем

24 Мб. Теперь объём загруженных за 10 недель данных составляет 2. Результат это уже весьма приличный, но тут возникает вопрос о том, можно ли сделать так, чтобы добиться результата, превышающего 50%. Это значит, что мы улучшили базовый показатель на 44%. Если подобное получится — это будет просто здорово.

▍Разбиение кода приложения на фрагменты

Вернёмся к файлу main.js, который несчастной Алисе приходится загружать постоянно.

Первый — это список товаров, второй — страница с подробными сведениями о товаре. Выше я говорил о том, что на нашем сайте имеется два самостоятельных раздела. Размер кода, уникального для каждого из них, составляет 25 Кб (а 150 Кб кода применяется и там и там).

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

Кроме того, как оказалось, у нас имеется огромный встроенный SVG-файл, используемый для рендеринга значков, который весит целых 25 Кб и изменяется редко.

С этим надо что-то делать.

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

module.exports = { entry: { main: path.resolve(__dirname, 'src/index.js'), ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'), ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'), Icon: path.resolve(__dirname, 'src/Icon/Icon.js'), }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash:8].js', }, plugins: [ new webpack.HashedModuleIdsPlugin(), // в результате хэши не будут неожиданно меняться ], optimization: { runtimeChunk: 'single', splitChunks: { chunks: 'all', maxInitialRequests: Infinity, minSize: 0, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name(module) { // получает имя, то есть node_modules/packageName/not/this/part.js // или node_modules/packageName const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]; // имена npm-пакетов можно, не опасаясь проблем, использовать // в URL, но некоторые серверы не любят символы наподобие @ return `npm.${packageName.replace('@', '')}`; }, }, }, }, },
};

Трудолюбивый Webpack, кроме того, создаст файлы для того, что является общим, например, у ProductList и ProductPage, то есть, дублирующегося кода тут не будет.

Обратите внимание на то, что файл с описанием значков мы отредактировали на шестой неделе. То, что мы только что сделали, позволит Алисе экономить почти каждую неделю по 50 Кб трафика. Вот наша традиционная таблица.

Объём данных, загруженных пользователем

815 Мб данных. Теперь за десять недель загружено всего 1. В соответствии с нашим теоретическим сценарием постоянный пользователь всегда будет работать с таким уровнем экономии. Это означает, что экономия трафика составила впечатляющие 56%.

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

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

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

▍Вопрос №1. Разве необходимость выполнения множества запросов не вредит скорости загрузки сайта?

На этот вопрос можно дать простой короткий ответ: «Нет, не вредит». Подобная ситуация выливалась в проблему в былые времена, когда в ходу был протокол HTTP/1.1, а при использовании HTTP/2 это уже неактуально.

Но в обоих этих материалах «слишком большое количество» означает «несколько сотен». Хотя, надо отметить, что в этом материале, опубликованном в 2016 году, и в этой статье Khan Academy 2015 года делаются выводы о том, что даже при использовании HTTP/2, использование слишком большого количества файлов замедляет загрузку. Поэтому стоит помнить о том, что если вам приходится работать с сотнями файлов, на скорость их загрузки могут повлиять ограничения на параллельную обработку данных.

Кроме того, я проводил всестороннее исследование среди тех, кто пользуется более старыми системами. Если интересно, поддержка HTTP/2 имеется в IE 11 в Windows 10. Они единодушно заявили, что их скорость загрузки веб-сайтов особенно не заботит.

▍Вопрос №2. В Webpack-бандлах есть вспомогательный код. Создаёт ли он дополнительную нагрузку на систему?

Да, это так.

▍Вопрос №3. При работе с множеством маленьких файлов ухудшается уровень их сжатия, не так ли?

Да, это тоже так. На самом деле, мне хотелось бы сказать вот что:

  • Больше файлов — значит больше вспомогательного Webpack-кода.
  • Больше файлов — это меньший уровень сжатия.

Давайте с этим разберёмся для того, чтобы понять, насколько это плохо.

Это добавило примерно 2% к объёму данных, отправляемых в браузер. Только что я провёл испытание, в ходе которого код из файла размером 190 Кб был разбит на 19 частей.

Нет, не стоит. В итоге получается, что при первом посещении сайта пользователь загрузит на 2% больше данных, а при последующих — на 60% меньше, и продолжаться это будет очень и очень долго.
Так стоит ли об этом беспокоиться?

1. Когда я проводил сравнение системы, использующей 1 файл, и системы с 19 файлами, я испытал её с использованием различных протоколов, в том числе и HTTP/1. Нижеприведённая таблица очень сильно поддерживает идею о том, что больше файлов — значит лучше.

Данные о работе с 2 версиями сайта, размещённого на статическом хостинге Firebase, код которого имеет размеры 190 Кб, но, в первом случае, упакован в 1 файл, а во втором — разбит на 19

При работе в 3G и 4G-сетях на загрузку варианта сайта с 19 файлами ушло на 30% меньше времени, чем на загрузку сайта с одним файлом.

Например, один сеанс загрузки сайта по 4G (Run 2 в таблице) занял 646 мс, ещё один (Run 4) — 1116 мс, что на 73% дольше. В данных, представленных в таблице, много шума. Поэтому возникает ощущение, что говорить о том, что HTTP/2 «на 30% быстрее» — это несколько нечестно.

Но, на самом деле, единственное, что тут можно сказать, заключается в том, что применение HTTP/2, вероятно, не особо заметно влияет на загрузку страниц. Я создал эту таблицу для того, чтобы посмотреть, что даёт использование HTTP/2.

Тут представлены результаты для не самой новой версии Windows с IE11 и HTTP/1. Настоящим сюрпризом стали две последних строчки в этой таблице. Я, если бы заранее пытался предсказать результаты испытания, точно сказал бы, что такая конфигурация будет загружать материалы гораздо медленнее других. 1. Правда, тут использовалось очень быстрое сетевое подключение, и мне, для подобных испытаний, вероятно, стоит пользоваться чем-то более медленным.

Я, чтобы исследовать мой сайт на совсем уж древней системе, загрузил виртуальную машину Windows 7 с сайта Microsoft. А теперь расскажу вам одну историю. Для этого я пошёл на страницу Microsoft, предназначенную для загрузки IE 9. Там был установлен IE8, который я решил обновить до IE9. Но сделать этого мне не удалось.

Вот незадача...

Если вы хотите поэкспериментировать — можете воспользоваться написанным мной небольшим HTTP/2 сервером с поддержкой кэша ответов, gzip и brotli. Кстати, если говорить об HTTP/2, хочется отметить, что этот протокол интегрирован в Node.js.

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

Теперь поговорим о разделении кода.

Разделение кода

Основная идея методики разделения кода звучит так: «Не загружайте ненужный код». Мне говорили, что использование этого подхода имеет смысл лишь для некоторых сайтов.

Если есть какая-то часть сайта, которую посещают лишь 20% пользователей, и её функционал обеспечивают более 20% JavaScript-кода сайта, тогда этот код нужно загружать только по запросу. Я предпочитаю, когда речь идёт о разделении кода, использовать правило 20/20, которое я только что сформулировал.

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

▍Разделять или нет?

Как найти ответ на вопрос о том, нужно вам разделение кода или нет? Предположим, у вас имеется интернет-магазин, и вы размышляете о том, надо ли отделить от остального кода тот код, который используется для приёма оплаты от покупателей, так как лишь 30% посетителей у вас что-то покупают.

Во-первых — вам стоило бы поработать над наполнением магазина и продавать что-то такое, что окажется интересным большему количеству посетителей сайта. Что тут сказать? Так как перед «разделением кода» следует всегда выполнять «разделение бандла», и вы, надеюсь, так и делаете, то вы, вероятно, уже знаете о том, какие размеры имеет интересующий нас код. Во-вторых — нужно понять то, какой объём кода совершенно уникален для того раздела сайта, где принимается оплата.

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

Размер остального кода сайта — 300 Кб. Итак, вы выяснили, что совершенно уникальный код раздела сайта, используемого для оплаты покупок, занимает 7 Кб. В подобной ситуации я не стал бы заниматься разделением кода по нескольким причинам:

  • Если загрузить эти 7 Кб заранее, сайт это не замедлит. Помните, что файлы загружаются параллельно и попробуйте измерить разницу, необходимую для загрузки 300 Кб и 307 Кб кода.
  • Если вы будете загружать этот код позже, тогда пользователю придётся ждать после нажатия на кнопку «Оплатить». А это ведь тот самый момент, когда вам надо, чтобы всё прошло настолько гладко, насколько это возможно.
  • Разделение кода требует внесения изменений в приложение. В коде, в тех местах, где раньше всё делалось синхронно, появляется асинхронная логика. Конечно, космических сложностей в подобных преобразованиях кода не наблюдается, но это всё равно дополнительный объём работы, который, как мне кажется, должен выполняться ради ощутимого улучшения впечатления пользователя от работы с сайтом.

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

А теперь рассмотрим пару примеров применения этой технологии.

▍Полифиллы

Я начинаю именно с этого примера, так как то, что мы тут рассмотрим, просто реализуется и применимо к большинству сайтов.

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

require('whatwg-fetch');
require('intl');
require('url-polyfill');
require('core-js/web/dom-collections');
require('core-js/es6/map');
require('core-js/es6/string');
require('core-js/es6/array');
require('core-js/es6/object');

Этот файл импортируется в самом начале кода файла index.js, который является точкой входа в приложение:

import './polyfills';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css'; const render = () => { ReactDOM.render(<App />, document.getElementById('root'));
} render(); // да, смысла во мне пока нет

Благодаря использованию конфигурации Webpack из предыдущего раздела, материалы полифиллов будут автоматически разделены на четыре файла, так как для их реализации используются четыре npm-пакета. Их размер составляет примерно 25 Кб, 90% браузеров они не нужны, поэтому имеет смысл загружать их динамически.

Благодаря применению Webpack 4 и использованию конструкции import() (не путайте её с ключевым словом import), организовать условную загрузку полифиллов очень просто:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css'; const render = () => { ReactDOM.render(<App />, document.getElementById('root'));
} if ( 'fetch' in window && 'Intl' in window && 'URL' in window && 'Map' in window && 'forEach' in NodeList.prototype && 'startsWith' in String.prototype && 'endsWith' in String.prototype && 'includes' in String.prototype && 'includes' in Array.prototype && 'assign' in Object && 'entries' in Object && 'keys' in Object
) { render();
} else { import('./polyfills').then(render);
}

Как видите, если всё, что нам надо, поддерживается — мы просто переходим к рендерингу. Если нет — импортируем полифиллы и уже после этого вызываем render(). Когда этот код выполняется в браузере, механизмы Webpack займутся загрузкой наших четырёх npm-пакетов, а когда они окажутся загружены и разобраны, будет осуществлён вызов render() и работа продолжится.

Кроме того, как сказано в документации к Webpack, команда import() использует промисы, поэтому полифилл для данной возможности нужно загружать отдельно от других полифиллов. Кстати сказать, для использования import() вам понадобится плагин Babel dynamic-import.

Рассмотрим теперь пример посложнее. Как я и говорил, это очень просто.

▍Динамическая загрузка материалов в React, основанная на маршрутах

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

Так как на сайте уже применяется разделение бандлов, я могу понять, что всё это занимает более 100 Кб. В этом разделе имеется множество замечательных возможностей, куча графиков и здоровенная npm-библиотека для работы с ними.

Webpack разделил всё по бандлам, поэтому ему нужно найти конструкцию import AdminPage from './AdminPage.js' и включить это в первоначальную загрузку сайта. В настоящий момент настройки маршрутизации сайта таковы, что, когда пользователь просматривает URL /admin, рендерится <AdminPage>.

Нам надо поместить ссылку на материалы для административной страницы в команду, выполняющую динамический импорт, наподобие import('./AdminPage.js'), в результате Webpack будет знать о том, что загружать эти материалы надо динамически. Однако нам это не нужно.

Причём, для этого не нужно заниматься конфигурированием.

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

import React from 'react'; class AdminPageLoader extends React.PureComponent { constructor(props) { super(props); this.state = { AdminPage: null, } } componentDidMount() { import('./AdminPage').then(module => { this.setState({ AdminPage: module.default }); }); } render() { const { AdminPage } = this.state; return AdminPage ? <AdminPage {...this.props} /> : <div>Loading...</div>; }
} export default AdminPageLoader;

В основе всего этого лежит весьма простая идея. Когда данный компонент монтируется (предполагается, что пользователь перешёл по URL /admin), мы динамически загружаем ./AdminPage.js, после чего сохраняем ссылку на этот компонент в состоянии приложения.

В методе render() мы просто выводим, ожидая загрузки <AdminPage>, надпись <div>Loading...</div>, или выводим <AdminPage> после того, как эта сущность будет загружена и сохранена в состоянии.

Я сделал всё это своими силами ради интереса, но в реальных проектах достаточно воспользоваться react-loadable, как описано в документации React по разделению кода.

Итоги

Полагаю, я рассказал всё, что хотел (хотя, надо отметить, мы не говорили тут о CSS). Подведём краткие итоги:

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

Уважаемые читатели! Используете ли вы разделение бандлов и разделение кода в своих проектах?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть