Оптимизация загрузки JavaScript: практики преодоления "тяжёлых" скриптов

Мобильное устройство пользователя с двухъядерным процессором и 2 ГБ памяти обрабатывает поток данных вашего одностраничного приложения. Браузер парсит 300 КБ сжатого JavaScript, запускает реактивную систему, инициализирует маршрутизацию, запрашивает данные с сервера. В какой-то момент главный поток блокируется на 1.2 секунды — и пользователь закрывает вкладку. Эту проблему нельзя исправить добавлением async к тегу скрипта. Нужна стратегия.

Анализ до оптимизации

Сначала измерим:

javascript
// Используем 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% времени загрузки занимает:

  1. Парсинг и компиляция скриптов (V8's Ignition и TurboFan)
  2. Выполнение инициализационного кода (конструкторы классов, провайдеры DI, валидаторы)
  3. Сериализация начального состояния приложения (избыточный JSON)

Трёхэтапное разделение кода

1. Гранулярный code splitting

Автоматическое разделение через динамический импорт недостаточно. Анализируем граф зависимостей:

bash
npx webpack-bundle-analyzer stats.json

Выявляем проблемные модули:

  • Неиспользуемые локали moment.js — подключаем локаль через webpack.ContextReplacementPlugin
  • Каталог utils/ объёмом 450 КБ — разделяем на core и дополнительные
  • Дубликаты библиотек: lodash и lodash-es — нормализуем импорты

Ручная стратегия разделения:

javascript
// 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 браузеры кэшируют скомпилированный байткод. Используем модульный подход:

html
<!-- Поддержка type="module" и nomodule для обратной совместимости -->
<script type="module" src="app.esm.js"></script>
<script nomodule src="app.legacy.js"></script>

В Webpack настраиваем двойную сборку:

javascript
output: {
  filename: ({ chunk }) => 
    chunk.name === 'esm' ? '[name].esm.js' : '[name].legacy.js'
}

Эффект: сокращение времени парсинга на 30% в современных браузерах.

3. Ленивая гидратация в SSR

Серверный рендеринг — не панацея. Применяем поэтапную гидратацию:

javascript
// Компонент активируется при попадании в 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:

javascript
import { formatDistanceToNow } from 'date-fns'; // 2 КБ вместо 65 КБ

Web Workers для тяжелых вычислений

Выносим сортировку крупных массивов и математические операции:

javascript
// 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);
});

Контроль в процессе разработки

  1. Интегрируем бюджеты производительности в сборку:
javascript
performance: {
  hints: 'error',
  maxAssetSize: 500 * 1024,
  maxEntrypointSize: 500 * 1024
}
  1. Настраиваем автоматический аудит в CI/CD:
yaml
- 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 должен доказывать своё право находиться в критическом пути загрузки. Инструменты — лишь средства. Главное — изменения в мышлении: от "работает" к "работает быстро".