javascript

Магия ClientOnly: повышаем производительность и безопасность в Nuxt-приложениях

  • суббота, 5 июля 2025 г. в 00:00:07
https://habr.com/ru/articles/924818/

Введение

Привет, хабровчане! 👋 Сегодня поговорим о компоненте ClientOnly в Nuxt, который я часто использую в своих проектах. И нет, это не потому что я не знаю как починить SSR-ошибки. Многие воспринимают его просто как костыль для решения проблем с SSR, но на самом деле этот инструмент может принести немало пользы с точки зрения производительности и даже безопасности. Давайте разбираться без лишней духоты и на реальных примерах!

Что такое ClientOnly и зачем он нужен?

ClientOnly — это встроенный компонент Nuxt, который позволяет рендерить содержимое только на стороне клиента. Если совсем просто: всё что внутри него, сервер не трогает, а браузер уже сам разбирается.

🧙‍♂️ Представьте, что ClientOnly — это как шапка-невидимка для вашего кода. Сервер просто говорит: "Я ничего не вижу, я ничего не знаю!" и перекладывает ответственность на клиент.

<template>
  <div>
    <h1>Моя крутая страница</h1>

    <!-- Этот контент рендерится и на сервере, и на клиенте -->
    <p>Этот текст видят все и сразу</p>

    <!-- А этот только на клиенте -->
    <ClientOnly>
      <FancyChart :data="chartData" />
      <template #fallback>
        <p>Загружаем крутой график...</p>
      </template>
    </ClientOnly>
  </div>
</template>

Как ClientOnly помогает с производительностью

1. Разгружаем сервер

Когда у вас на странице есть тяжелые компоненты (например, интерактивные графики или карты), их рендеринг на сервере может существенно замедлить ответ. С ClientOnly сервер просто пропускает эти части:

<template>
  <div class="dashboard">
    <!-- Быстрая статическая часть -->
    <HeaderStats :numbers="quickStats" />

    <!-- Тяжелая часть рендерится только в браузере -->
    <ClientOnly>
      <InteractiveMap :points="mapData" />
    </ClientOnly>
  </div>
</template>

🏋️‍♂️ "Извини, сервер, но эту тяжесть понесет браузер пользователя. У тебя и так работы много!"

2. Ускоряем первую отрисовку

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

<template>
  <article>
    <!-- Критичный контент отдаём сразу -->
    <h1>{{ article.title }}</h1>
    <div v-html="article.content"></div>

    <!-- А комментарии пусть подождут -->
    <ClientOnly>
      <CommentsSection :article-id="article.id" />
      <template #fallback>
        <p>Загружаем комментарии... ~~или нет, если это токсичные комментарии о PHP~~</p>
      </template>
    </ClientOnly>
  </article>
</template>

3. Избегаем проблем гидратации

Если у вас компонент, который может по-разному выглядеть на сервере и клиенте, ClientOnly спасёт от проблем с гидратацией и ошибок в консоли:

<script setup>
const isMobile = ref(false);

// Без ClientOnly тут была бы ошибка гидратации
onMounted(() => {
  isMobile.value = window.innerWidth < 768;
});
</script>

<template>
  <ClientOnly>
    <MobileMenu v-if="isMobile" />
    <DesktopMenu v-else />
  </ClientOnly>
</template>

🚫 Ошибки гидратации — как несовпадение носков. В темноте (на сервере) казалось, что они одинаковые, а при свете дня (в браузере) оказывается, что один синий, а другой в красную полоску. ClientOnly — это как решение вообще не надевать носки, пока не включишь свет.

Безопасность через ClientOnly — неочевидное преимущество

1. Защита от примитивного скрапинга

Боты и парсеры, которые не выполняют JavaScript, не увидят содержимое внутри ClientOnly:

<template>
  <div>
    <!-- Это увидят все -->
    <p>Свяжитесь с нами для получения демо-доступа</p>

    <!-- А это только реальные пользователи -->
    <ClientOnly>
      <EmailForm subject="Запрос демо-доступа" />
    </ClientOnly>
  </div>
</template>

🕵️‍♂️ "Уважаемые боты, тут нет той формы, которую вы ищете... Проходите мимо!"

2. Скрываем чувствительные элементы интерфейса

Если хотите скрыть от поверхностного анализа админ-функции или другие элементы управления:

<template>
  <div class="product-card">
    <img :src="product.image" />
    <h3>{{ product.name }}</h3>
    <p>{{ product.price }} ₽</p>

    <!-- Админ-функции не будут видны в исходном HTML -->
    <ClientOnly>
      <div
        v-if="isAdmin"
        class="admin-controls"
      >
        <button @click="editProduct">Редактировать</button>
        <button @click="deleteProduct">Удалить</button>
      </div>
    </ClientOnly>
  </div>
</template>

👑 Админ-панель как Fight Club — первое правило: никто не говорит об админ-панели (особенно исходный HTML).

3. Защита от спам-ботов

Простые спам-боты не смогут найти формы обратной связи, если они обернуты в ClientOnly:

<template>
  <div class="contact-section">
    <h2>Свяжитесь с нами</h2>

    <ClientOnly>
      <ContactForm />
      <template #fallback>
        <p>Загрузка формы...</p>
      </template>
    </ClientOnly>
  </div>
</template>

🤖 *Спам-бот: "Форма? Какая форма? Я ничего не вижу!"
👨‍💻 *Разработчик: "Именно так, дружок, именно так..."*

4. Дополнительный уровень защиты с Lazy и отложенной гидратацией

Комбинируя ClientOnly с Lazy-префиксом и стратегиями отложенной гидратации, можно создать еще более сложный барьер для ботов:

<template>
  <div class="protected-section">
    <h2>Конфиденциальная информация</h2>

    <!-- Компонент грузится лениво (при необходимости) -->
    <ClientOnly>
      <!-- Гидратация только когда элемент виден в области просмотра -->
      <LazySecureContent hydrate-on-visible />

      <!-- Альтернативно: гидратация по взаимодействию -->
      <LazyApiKeyDisplay hydrate-on-interaction="click" />

      <!-- Показывается пока компоненты не загружены -->
      <template #fallback>
        <p>Загрузка защищенного контента... ~~или секретный план по захвату мира~~</p>
      </template>
    </ClientOnly>
  </div>
</template>

В этом примере мы создаем многоуровневую защиту:

  1. Компонент рендерится только на клиенте благодаря ClientOnly 🛡️

  2. Код компонента загружается лениво благодаря префиксу Lazy 😴

  3. Компоненты гидратируются (становятся интерактивными) только при определенных условиях:

    • LazySecureContent - только когда виден пользователю 👁️

    • LazyApiKeyDisplay - только после клика 👆

Для автоматизированных ботов такой подход создает серьезные препятствия, так как требует:

  • Выполнения JavaScript (бот: "Погодите, я должен запустить движок V8? У меня лапки!!")

  • Эмуляции скролла для достижения видимости компонента (бот: "Как-как скроллить? Я не умею!")

  • Эмуляции взаимодействия пользователя (бот: "Вы хотите, чтобы я еще и кликал?! Я увольняюсь!")

🏰 Это как трехслойная защита замка: мост с крокодилами (ClientOnly), высокие стены с часовыми (Lazy) и потайная дверь с паролем (стратегии гидратации). Попробуй пройди!

Nuxt поддерживает множество стратегий гидратации:

  • hydrate-on-visible - при появлении в области видимости

  • hydrate-on-idle - когда браузер простаивает (идеально для ленивых компонентов, как я по понедельникам)

  • hydrate-on-interaction - после взаимодействия (клик, наведение) (играем в "кликни меня, если осмелишься")

  • hydrate-on-media-query - при соответствии медиа-запросу (только для посетителей с маленькими экранами и большими амбициями)

  • hydrate-after - после указанной задержки (как дошик — залил и жди 3 минуты)

  • hydrate-when - при выполнении условия (когда луна в седьмом доме, а Юпитер выровнялся с Марсом)

  • hydrate-never - никогда не гидратировать (идеально для компонентов, которые должны оставаться такими же статичными, как мимика покерфейса)

Вы можете комбинировать эти стратегии для создания оптимальной защиты и производительности.

Реальные кейсы использования

Интеграция сторонних библиотек без SSR-поддержки

Когда вы притворяетесь, что компонент не работает на SSR, потому что вам лень его фиксить

<script setup>
  import { onMounted, ref } from 'vue';

  const chartRef = ref(null);
  let myChart = null;

  onMounted(() => {
    // Подключаем Chart.js без проблем с SSR
    import('chart.js').then(({ Chart }) => {
      myChart = new Chart(chartRef.value, {
        type: 'bar',
        data: {
          labels: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт'],
          datasets: [
            {
              label: 'Продажи',
              data: [12, 19, 3, 5, 2],
            },
          ],
        },
      });
    });
  });
</script>

<template>
  <ClientOnly>
    <canvas ref="chartRef"></canvas>
    <template #fallback>
      <div class="chart-placeholder"> График загружается... *(или мы так говорим, чтобы вы подождали)* </div>
    </template>
  </ClientOnly>
</template>

📊 Некоторые библиотеки так же совместимы с SSR, как кошки с водой — теоретически можно, но вы точно хотите провести весь день, вытирая последствия?

Компоненты с геолокацией

<script setup>
  const userLocation = ref(null);
  const isLoading = ref(true);

  onMounted(async () => {
    try {
      const position = await new Promise((resolve, reject) => {
        navigator.geolocation.getCurrentPosition(resolve, reject);
      });

      userLocation.value = {
        lat: position.coords.latitude,
        lng: position.coords.longitude,
      };
    } catch (err) {
      console.error('Не удалось получить местоположение', err);
    } finally {
      isLoading.value = false;
    }
  });
</script>

<template>
  <ClientOnly>
    <div v-if="isLoading">Определяем ваше местоположение... *(и нет, мы не следим за вами... наверное)*</div>
    <div v-else-if="userLocation">
      <p>Ваши координаты: {{ userLocation.lat }}, {{ userLocation.lng }}</p>
      <NearestShops :location="userLocation" />
    </div>
    <div v-else> Не удалось определить местоположение. *(Похоже, вы в бункере или просто очень не хотите, чтобы мы знали, где вы!)* </div>
  </ClientOnly>
</template>

🌍 Без ClientOnly ваш сервер бы пытался узнать свою геолокацию. "Где я? Кто я? В каком дата-центре меня разместили?"

Оптимизация виджетов аналитики

<script setup>
  function initAnalytics() {
    // Инициализация аналитики только на клиенте
    window.dataLayer = window.dataLayer || [];
    function gtag() {
      dataLayer.push(arguments);
    }
    gtag('js', new Date());
    gtag('config', 'G-XXXXXXXXXX');

    // Вставляем скрипт
    const script = document.createElement('script');
    script.async = true;
    script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX';
    document.head.appendChild(script);
  }

  onMounted(() => {
    // Запускаем аналитику с небольшой задержкой
    // чтобы не блокировать основной рендеринг
    setTimeout(initAnalytics, 1000);
  });
</script>

<template>
  <ClientOnly>
    <!-- Аналитика не замедляет SSR и не вызывает ошибок гидратации -->
    <!-- И да, мы следим за каждым вашим кликом! Мухахаха! (шутка) -->
  </ClientOnly>
</template>

👀 Google Analytics уже достаточно замедляет ваш сайт на клиенте. Нет смысла замедлять еще и сервер!

Когда НЕ стоит использовать ClientOnly

Несмотря на все преимущества, ClientOnly имеет свои минусы:

  1. SEO-контент - поисковые боты могут не увидеть то, что внутри ClientOnly (Google-бот: "Я не вижу контента, значит его не существует!")

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

  3. Мелкие компоненты - для простых элементов оверхед от дополнительной логики может быть избыточным (как использовать экскаватор, чтобы посадить цветок в горшке)

Заключение

ClientOnly в Nuxt — это не просто способ обойти ошибки SSR (хотя признайтесь, именно поэтому вы начали его использовать!), а мощный инструмент для оптимизации производительности и даже добавления базового уровня защиты. Умело используя его, вы можете:

  • Ускорить загрузку страницы ⚡

  • Снизить нагрузку на сервер 🖥️

  • Защитить формы от примитивных спам-ботов 🛡️

  • Скрыть чувствительные элементы интерфейса 🕵️

А главное — все это встроено в Nuxt из коробки, не требует дополнительных библиотек и настроек! Это как получить швейцарский нож, когда просто просили открывашку.

Всем спасибо за прочтение, я малость зае***ся устал. Я пошел отдыхать)

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Так как я экспериментирую со стилем, скажите норм ли с юмором и уместно ли, может быть разгрузило чтение и тд тп
75% Да3
25% Нет1
Проголосовали 4 пользователя. Воздержался 1 пользователь.