Мобильное устройство пользователя с двухъядерным процессором и 2 ГБ памяти обрабатывает поток данных вашего одностраничного приложения. Браузер парсит 300 КБ сжатого JavaScript, запускает реактивную систему, инициализирует маршрутизацию, запрашивает данные с сервера. В какой-то момент главный поток блокируется на 1.2 секунды — и пользователь закрывает вкладку. Эту проблему нельзя исправить добавлением async
к тегу скрипта. Нужна стратегия.
Анализ до оптимизации
Сначала измерим:
// Используем Navigation Timing API для базовых метрик
const [entry] = performance.getEntriesByType("navigation");
console.log({
DOMInteractive: entry.domInteractive,
DOMComplete: entry.domComplete,
LoadEventEnd: entry.loadEventEnd
});
// Замер времени длительных задач (Long Tasks)
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.log(`Блокировка на ${entry.duration} мс`, entry.startTime);
}
});
observer.observe({ entryTypes: ["longtask"] });
По данным Chrome DevTools, 40% времени загрузки занимает:
- Парсинг и компиляция скриптов (V8's Ignition и TurboFan)
- Выполнение инициализационного кода (конструкторы классов, провайдеры DI, валидаторы)
- Сериализация начального состояния приложения (избыточный JSON)
Трёхэтапное разделение кода
1. Гранулярный code splitting
Автоматическое разделение через динамический импорт недостаточно. Анализируем граф зависимостей:
npx webpack-bundle-analyzer stats.json
Выявляем проблемные модули:
- Неиспользуемые локали
moment.js
— подключаем локаль через webpack.ContextReplacementPlugin - Каталог
utils/
объёмом 450 КБ — разделяем на core и дополнительные - Дубликаты библиотек:
lodash
иlodash-es
— нормализуем импорты
Ручная стратегия разделения:
// routes.js
const CheckoutWizard = () => import(
/* webpackChunkName: "checkout" */
/* webpackPrefetch: true */
'./CheckoutWizard.vue'
);
// webpack.config.js
optimization.splitChunks({
cacheGroups: {
framework: {
test: /[\\/]node_modules[\\/](react|vue|svelte)[\\/]/,
chunks: 'all'
}
}
});
2. Проактивное кэширование парсера
Modern браузеры кэшируют скомпилированный байткод. Используем модульный подход:
<!-- Поддержка type="module" и nomodule для обратной совместимости -->
<script type="module" src="app.esm.js"></script>
<script nomodule src="app.legacy.js"></script>
В Webpack настраиваем двойную сборку:
output: {
filename: ({ chunk }) =>
chunk.name === 'esm' ? '[name].esm.js' : '[name].legacy.js'
}
Эффект: сокращение времени парсинга на 30% в современных браузерах.
3. Ленивая гидратация в SSR
Серверный рендеринг — не панацея. Применяем поэтапную гидратацию:
// Компонент активируется при попадании в viewport
const LazySearch = dynamic(
() => import('./Search').then(mod => mod.Search),
{
ssr: false,
loading: () => <Skeleton width={300} height={45} />
}
);
// В серверной сборке заменяем тяжёлые части на шаблоны
if (typeof window === 'undefined') {
BigComponent = require('./BigComponent.server').default;
}
Оптимизация времени выполнения
Деструктуризация vs классический API
Библиотеки типа Moment.js заменяем на date-fns с tree-shaking:
import { formatDistanceToNow } from 'date-fns'; // 2 КБ вместо 65 КБ
Web Workers для тяжелых вычислений
Выносим сортировку крупных массивов и математические операции:
// main.js
const worker = new Worker('./dataProcessor.js');
worker.postMessage({ data: largeDataSet });
worker.onmessage = ({ data }) => {
updateUI(data);
};
// dataProcessor.js
self.addEventListener('message', ({ data }) => {
const result = performHeavyTask(data);
self.postMessage(result);
});
Контроль в процессе разработки
- Интегрируем бюджеты производительности в сборку:
performance: {
hints: 'error',
maxAssetSize: 500 * 1024,
maxEntrypointSize: 500 * 1024
}
- Настраиваем автоматический аудит в CI/CD:
- name: Lighthouse Audit
uses: treosh/lighthouse-ci-action@v3
with:
urls: |
https://staging.example.com/
budgetPath: ./lighthouse-budget.json
Неочевидные ловушки
- Забытые polyfills: 1 КБ полифила для
Array.prototype.includes
увеличивает время выполнения на старых устройствах на 120 мс; - Агрессивная предзагрузка:
preload
для шрифтов блокирует рендеринг, если они не используются в начальном viewport; - CSS-in-JS overhead: Runtime библиотек вроде styled-components добавляет 30-50 мс на инициализацию.
Осмысленная оптимизация требует не слепого применения шаблонов, а системного подхода к архитектуре. Каждый килобайт JavaScript должен доказывать своё право находиться в критическом пути загрузки. Инструменты — лишь средства. Главное — изменения в мышлении: от "работает" к "работает быстро".