Intentional render blocking: когда блокировать рендеринг — это feature

#performance#rendering#javascript

На каждом code review, где я вижу <script> в <head> без async/defer, у меня срабатывает conditioned reflex — комментить про render blocking. Но за последний год я трижды осознанно нарушал это правило. Вот когда синхронное исполнение JS до начала рендеринга — это не technical debt, а точный UX-расчёт.

Почему мы default’но избегаем render blocking

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

<script async src="..."></script> <!-- Загружает без блокировки, выполняет ASAP -->
<script defer src="..."></script> <!-- Загружает без блокировки, выполняет после DOMContentLoaded -->

Особенно критично для third-party-скриптов (аналитика, реклама, виджеты). Но есть edge cases, где блокировка даёт предсказуемое поведение.

Кейс 1: Instant layout shift prevention

Представьте Single Page Application, где routing происходит на клиенте. Без синхронной проверки auth-статуса в <head> вы можете:

  1. Отрисовать публичный layout
  2. Получить ответ от API, что юзер авторизован
  3. Сделать violent reflow в private UI
// В head, без defer/async
if (checkAuthCookie()) {
  document.documentElement.classList.add('authenticated');
  // CSS-переменные для авторизованного состояния
  document.documentElement.style.setProperty('--nav-bg', '#333');
}

Этот подход мы применили в admin-панели, где flash of public UI выглядел как security issue. Задержка в 200ms до начала рендеринга — acceptable trade-off.

Кейс 2: Critical feature detection

В одном из проектов мы обнаружили, что 5% юзеров получают broken UI из-за отсутствия WebGL support. Проблема в том, что:

Решение — синхронный скрипт в <head>:

const isWebGLAvailable = (() => {
  try {
    return !!window.WebGLRenderingContext && !!document.createElement('canvas').getContext('webgl');
  } catch (e) {
    return false;
  }
})();

if (!isWebGLAvailable) {
  window.location.href = '/fallback';
}

Когда это точно anti-pattern

Не используйте render blocking для:

В нашем случае из трёх инстансов два были связаны с auth flow, один — с hardware capabilities. Во всех случаях UX-выгода перевешивала перформанс-пеналити.

Как измерить impact

Даже осознанная блокировка требует мониторинга. Включите в ваш CI:

// Puppeteer example
const blockingTime = await page.evaluate(() => {
  return Math.max(
    performance.getEntriesByType('navigation')[0].domContentLoadedEventEnd,
    performance.getEntriesByType('paint')[0].startTime
  );
});

if (blockingTime > 300) {
  throw new Error(`Render blocking exceeded threshold: ${blockingTime}ms`);
}

Если ваш скрипт тормозит рендеринг больше чем на 200-300ms — время оптимизировать или пересмотреть подход. В наших кейсах блокировка укладывалась в 150ms даже на low-end devices.

Render blocking — как !important в CSS. Избегайте по дефолту, но держите в арсенале для особых случаев. Главное — document your decisions, чтобы следующий разработчик не “пофиксил” это рандомным добавлением async в CI.


Источник: https://www.jayfreestone.com/writing/intentional-render-blocking-javascript/