Современные веб-приложения часто состоят из десятков и сотен модулей. Если весь код объединяется в один большой JavaScript-файл, браузеру приходится загружать значительный объём данных ещё до начала работы приложения. Это увеличивает время первоначальной загрузки, ухудшает пользовательский опыт и снижает производительность на медленных сетях.
Для решения этой проблемы используются разделение кода (Code Splitting) и динамический импорт (Dynamic Import). Parcel автоматически поддерживает эти механизмы и позволяет разбивать приложение на независимые части без сложной настройки.
Рассмотрим приложение со следующей структурой:
src/
├── index.js
├── dashboard.js
├── reports.js
├── charts.js
└── editor.js
Главный файл подключает все модули:
import './dashboard';
import './reports';
import './charts';
import './editor';
После сборки Parcel создаст один общий бандл:
dist/
└── index.js
Даже если пользователь никогда не откроет раздел отчётов или редактор, весь связанный код будет загружен сразу.
Недостатки такого подхода:
Code Splitting — это процесс разделения приложения на несколько независимых файлов, которые загружаются по мере необходимости.
Вместо единого файла:
index.js
можно получить:
index.js
dashboard.js
reports.js
editor.js
Каждый фрагмент загружается только тогда, когда становится нужен приложению.
Parcel автоматически создаёт дополнительные чанки при обнаружении динамического импорта.
Стандарт ECMAScript предоставляет специальную функцию:
import()
Она возвращает Promise и позволяет загружать модуль во время выполнения программы.
Пример:
const module = await import('./editor.js');
Parcel анализирует подобные конструкции и автоматически создаёт отдельный файл для соответствующего модуля.
import { openEditor } from './editor';
openEditor();
Модуль включается в основной бандл.
const editor = await import('./editor');
editor.openEditor();
Модуль становится отдельным чанком и загружается только при необходимости.
Исходный код:
button.addEventListener('click', async () => {
const editor = await import('./editor');
editor.openEditor();
});
После сборки Parcel может сформировать структуру:
dist/
├── index.js
├── editor.a1b2c3.js
└── index.html
Файл редактора будет загружен браузером только после нажатия кнопки.
Наиболее распространённый сценарий — загрузка тяжёлых компонентов только тогда, когда пользователь начинает с ними работать.
Например:
async function showSettings() {
const settings = await import('./settings');
settings.render();
}
До вызова функции код настроек не загружается.
Поскольку import() возвращает Promise, возможна традиционная обработка через then.
import('./editor')
.then(module => {
module.openEditor();
});
Эквивалентный вариант через async/await:
async function open() {
const module = await import('./editor');
module.openEditor();
}
В современных приложениях обычно используется второй подход благодаря лучшей читаемости.
При динамической загрузке следует учитывать возможность возникновения ошибок:
try {
const editor = await import('./editor');
editor.openEditor();
} catch (error) {
console.error('Не удалось загрузить редактор', error);
}
Причинами ошибки могут быть:
Многие SPA-приложения используют динамический импорт для маршрутизации.
Пример простого роутера:
async function navigate(route) {
switch (route) {
case '/':
return import('./pages/home');
case '/about':
return import('./pages/about');
case '/contacts':
return import('./pages/contacts');
}
}
Parcel создаст отдельный файл для каждой страницы.
В результате пользователь загружает только текущий маршрут, а остальные страницы подгружаются позже.
src/
├── pages/
│ ├── home.js
│ ├── about.js
│ └── contacts.js
├── router.js
└── index.js
Роутер:
export async function loadPage(name) {
return import(`./pages/${name}.js`);
}
Parcel анализирует подобные конструкции и создаёт необходимые чанки для доступных модулей.
Очень часто код загружается по событиям интерфейса.
Например, открытие модального окна:
button.addEventListener('click', async () => {
const modal = await import('./modal');
modal.show();
});
Или запуск графического редактора:
launchEditor.addEventListener('click', async () => {
const editor = await import('./editor');
editor.start();
});
Такой подход позволяет существенно сократить объём первоначальной загрузки.
Некоторые библиотеки могут занимать сотни килобайт.
Например:
Вместо статического подключения:
import Chart from 'chart.js';
можно использовать динамическое:
async function loadCharts() {
const Chart = await import('chart.js');
return Chart;
}
Parcel выделит библиотеку в отдельный чанк.
Загрузка может выполняться не только для локальных файлов.
Пример:
const lodash = await import('lodash');
После сборки Parcel создаст отдельный пакетный чанк.
Это особенно полезно для редко используемых зависимостей.
Иногда необходимо ускорить будущую загрузку модуля.
Предположим, пользователь открыл страницу профиля и с высокой вероятностью перейдёт в настройки.
В этом случае можно инициировать загрузку заранее:
const settingsPromise = import('./settings');
Позже:
const settings = await settingsPromise;
Если файл уже скачан браузером, получение модуля произойдёт практически мгновенно.
После первого импорта модуль сохраняется в кэше JavaScript.
Пример:
const first = await import('./editor');
const second = await import('./editor');
Модуль будет загружен с сервера только один раз.
Второй вызов использует уже загруженную версию.
Это важная особенность механизма ES Modules.
Рассмотрим структуру:
import('./pageA');
import('./pageB');
Обе страницы используют:
import { formatDate } from './utils';
Parcel способен определить общий код и вынести его в отдельный чанк.
Условно структура может выглядеть так:
index.js
pageA.js
pageB.js
shared.js
В результате:
Во время сборки Parcel строит граф зависимостей.
Упрощённый пример:
index.js
├── home.js
├── about.js
└── editor.js
└── toolbar.js
Для динамических импортов формируются отдельные точки входа.
Граф может быть преобразован в набор независимых чанков, загружаемых по требованию.
Применение динамического импорта позволяет:
Вместо:
1.5 MB
можно получить:
300 KB
при первой загрузке.
Браузеру требуется меньше времени на анализ кода.
Неиспользуемые модули не загружаются заранее.
Особенно заметно влияние на:
Наиболее подходящие случаи:
import('./admin');
import('./feedback-form');
import('chart.js');
import('./report-generator');
import('./visual-editor');
import('./analytics');
Не стоит выносить в отдельные чанки:
Избыточное разделение может привести к появлению слишком большого количества сетевых запросов.
Главный модуль:
const button = document.querySelector('#load-report');
button.addEventListener('click', async () => {
const report = await import('./report');
report.generate();
});
Модуль отчётов:
export function generate() {
console.log('Генерация отчёта');
}
После сборки Parcel создаст отдельный чанк для
report.js.
Пока пользователь не нажмёт кнопку, код генерации отчётов не будет загружен.
Динамический импорт легко интегрируется с запросами к серверу.
async function openStatistics() {
const [stats, chartModule] = await Promise.all([
fetch('/api/stats').then(r => r.json()),
import('./charts')
]);
chartModule.render(stats);
}
В данном примере данные и код загружаются параллельно, что ускоряет отображение статистики.
Хорошо:
import('./editor');
import('./dashboard');
import('./reports');
Плохо:
import('./smallHelper');
import('./tinyFunction');
import('./miniTool');
Каждая страница приложения может быть отдельным чанком.
Особенно актуально для:
try {
const module = await import('./editor');
} catch (error) {
console.error(error);
}
После внедрения Code Splitting важно контролировать:
Parcel автоматизирует большую часть процессов разделения кода,
позволяя строить производительные приложения с минимальной
конфигурацией. Достаточно использовать стандартный механизм
import(), после чего сборщик самостоятельно создаёт
необходимые чанки, оптимизирует граф зависимостей и организует загрузку
модулей по требованию.