Сторонние скрипты Next.js 15: CWV без просадки — KEL IT
Сайты 8 мин чтения

Сторонние скрипты на лендинге Next.js 15: next/script без просадки Core Web Vitals

Лендинг отдаётся за 1,2 с: hero на месте, LCP в зелёной зоне. Потом в <head> подключают Google Analytics, Intercom, Facebook Pixel и Hotjar — и через неделю Search Console показывает INP «Poor» на мобильных, а Lighthouse ругается на «Reduce JavaScript execution time». Пользователь жмёт CTA, страница «замирает» на 300 мс: main thread занят чужим кодом.

Сторонние скрипты — главный источник регрессий Core Web Vitals после того, как вы уже оптимизировали next/image, next/font и framer-motion. Они не попадают в ваш bundle, но конкурируют за CPU, блокируют парсинг и вешают долгие обработчики на клики. Google оценивает страницу по полевым данным CrUX, а не по «чистому» lab-замеру без виджетов.

В этой статье — production-подход для Next.js 15 App Router: стратегии next/script, отложенная загрузка аналитики, Partytown для «тяжёлых» тегов, размещение чата below-the-fold и чеклист перед релизом. Цель — оставить GA4, пиксели и support-виджет, не убив INP и LCP.

Почему third-party ломает INP и LCP

INP (Interaction to Next Paint) измеряет задержку от действия пользователя до следующего кадра. Сторонние скрипты ухудшают его тремя путями:

  1. Long tasks — синхронный парсинг и выполнение JS в main thread при загрузке и после idle.
  2. Event listeners — глобальные обработчики click, scroll, touchstart с дорогой логикой (heatmap, session replay).
  3. Конкуренция за сеть — скрипты в <head> без defer откладывают discovery hero и шрифтов.

LCP страдает реже, но тоже: render-blocking script в <head>, синхронный GTM snippet, или чат-виджет, который вставляет iframe above-the-fold до отрисовки hero.

СервисТипичный вес (gzip)Риск для CWV
Google Analytics 4 (gtag)~45–90 KBINP, long tasks
Google Tag Managerконтейнер + тегиLCP, INP
Meta Pixel~30–80 KBINP, privacy
Intercom / Crisp / Carrot150–400 KB+INP, LCP (launcher)
Hotjar / Clarity80–200 KBINP (scroll handlers)

На SEO-лендинге услуг допустимы аналитика и один пиксель конверсии. Session replay и чат на первом экране — осознанный trade-off: измеряйте INP после каждого нового тега.

next/script: стратегии и порядок загрузки

next/script в App Router не просто обёртка над <script>: Next.js координирует порядок выполнения относительно гидрации и не дублирует скрипты при client navigation.

Четыре стратегии

strategyКогда грузитсяДля чего на лендинге
beforeInteractiveДо гидрации, в корневом layoutПочти никогда; критичный polyfill
afterInteractiveСразу после гидрации (default)GTM, если без него не обойтись
lazyOnloadПосле loadGA4, пиксели, большинство тегов
workerWeb Worker (Partytown)GA4, FB Pixel при жёстком INP

Рекомендуемый паттерн для GA4

// app/components/analytics.tsx — Client Component
'use client';

import Script from 'next/script';

const GA_ID = process.env.NEXT_PUBLIC_GA_ID;

export function Analytics() {
  if (!GA_ID) return null;

  return (
    <>
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
        strategy="lazyOnload"
      />
      <Script id="ga4-init" strategy="lazyOnload">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', '${GA_ID}', {
            send_page_view: true,
            anonymize_ip: true
          });
        `}
      </Script>
    </>
  );
}

Подключайте <Analytics /> в app/layout.tsx после {children}, не в <head> вручную:

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ru">
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

Не используйте beforeInteractive для аналитики: она не нужна до первого paint и ухудшает LCP.

События конверсии без блокировки клика

Отправка gtag('event', ...) из обработчика CTA может удлинить INP, если main thread занят. Паттерн:

function handleCtaClick() {
  // Сначала навигация или открытие модалки — UX
  router.push('/contact');

  // Аналитика — в requestIdleCallback или после microtask
  if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
    requestIdleCallback(() => {
      window.gtag?.('event', 'cta_click', { event_category: 'hero' });
    });
  }
}

Для критичных конверсий в рекламе Meta Pixel иногда требует синхронного fbq — тогда один пиксель, lazyOnload, без дублирования через GTM и прямой snippet одновременно.

Если нужна подобная разработка — напишите в Telegram.

Google Tag Manager: когда нужен, когда вреден

GTM удобен маркетингу: новые теги без деплоя. Для Core Web Vitals на лендинге это часто антипаттерн: контейнер грузится afterInteractive, внутри — десяток тегов, каждый добавляет long task.

Правила для SEO-лендинга на Vercel:

  1. Предпочитайте прямой gtag вместо GTM, если тегов ≤ 3 (GA4 + один Ads + один Pixel).
  2. Если GTM обязателен — один контейнер, strategy="lazyOnload", триггеры только на DOM Ready / Window Loaded, не на All Pages + custom HTML sync.
  3. Запретите в GTM Custom HTML с <script> без defer и теги «Facebook Pixel» + «GA4» + «LinkedIn» на одной странице без приоритизации.
  4. Включите Consent Mode v2 до загрузки рекламных тегов — меньше лишних запросов до согласия.
<Script id="gtm" strategy="lazyOnload">
  {`
    (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','${GTM_ID}');
  `}
</Script>

Альтернатива для 2026 года: server-side GTM (sGTM на Cloud Run / Stape) + клиентский лёгкий транспорт — события уходят с edge, браузер не тянет полный gtag.js. Сложнее в настройке, но INP стабильнее на мобильных.

Partytown и чат-виджеты: вынос main thread

Partytown (@builder.io/partytown) переносит совместимые third-party скрипты в Web Worker. Next.js поддерживает strategy="worker" при настройке Partytown в next.config.

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  experimental: {
    nextScriptWorkers: true,
  },
};

export default nextConfig;
<Script
  src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
  strategy="worker"
/>

Не все теги совместимы: чат-виджеты с Shadow DOM и прямым доступом к document часто ломаются в worker. Для Intercom/Crisp используйте другой подход:

  • Lazy mount — рендерить launcher только после load + requestIdleCallback или по scroll 25%.
  • Не ставить default bubble в углу hero: перекрытие CTA бьёт и по UX, и по CLS, если виджет резервирует место поздно.
  • Facade — своя кнопка «Написать в Telegram» (статическая, нулевой JS), а чат SDK подгружать по клику.
'use client';

import { useEffect, useState } from 'react';
import Script from 'next/script';

export function ChatLoader() {
  const [enabled, setEnabled] = useState(false);

  useEffect(() => {
    const idle = () => setEnabled(true);
    if ('requestIdleCallback' in window) {
      requestIdleCallback(idle, { timeout: 4000 });
    } else {
      setTimeout(idle, 3500);
    }
  }, []);

  if (!enabled) {
    return (
      <button
        type="button"
        className="fixed bottom-4 right-4 z-50 rounded-full bg-primary px-4 py-3 shadow-lg"
        onClick={() => setEnabled(true)}
        aria-label="Открыть чат"
      >
        Чат
      </button>
    );
  }

  return (
    <Script
      src="https://widget.intercom.io/widget/APP_ID"
      strategy="lazyOnload"
      onLoad={() => {
        window.Intercom?.('boot', { app_id: 'APP_ID' });
      }}
    />
  );
}

Так вы не платите 200 KB парсинга до первого взаимодействия с лендингом.

Мониторинг: @vercel/speed-insights и регрессии после маркетинга

Оптимизация без полевых данных — слепая. Подключите @vercel/speed-insights рядом с Analytics: он собирает RUM по LCP, INP, CLS на реальном трафике Vercel.

npm i @vercel/speed-insights
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ru">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

Процесс в команде:

  1. Baseline — зафиксируйте INP/LCP в Search Console до подключения GTM.
  2. Change log — каждый новый тег в GTM = запись в Notion + дата; через 28 дней сравните CrUX.
  3. Lab gate — Lighthouse CI на PR: порог «Total Blocking Time» и «Script Evaluation»; падение блокирует merge.
  4. Сегментация — Speed Insights по страницам: / vs /blog/*; блог часто без чата, лендинг — с полным набором тегов.

Связь с edge: HTML лендинга с PPR/ISR отдаётся быстро (низкий TTFB), но third-party не кешируется на Vercel Edge — они всегда идут с доменов Google/Meta. Выигрыш только в отложенной загрузке и минимизации количества.

Нужна помощь? Telegram → или vic.kell@ya.ru

FAQ

Можно ли вставить скрипт через dangerouslySetInnerHTML в layout?

Можно, но вы теряете стратегии next/script, дедупликацию и контроль порядка. Для любого внешнего JS используйте next/script или server-side транспорт событий.

lazyOnload теряет первые pageview?

GA4 с lazyOnload может задержить первый page_view на доли секунды — для SPA с client navigation настройте отдельный page_view в useEffect при смене pathname. На статическом лендинге услуг потеря минимальна; для рекламных landing с bounce < 3 с рассмотрите afterInteractive только для gtag, остальное — lazy.

Сколько сторонних скриптов «норма» на лендинге?

Ориентир: ≤ 150 KB суммарного сжатого JS third-party до load, не более 3–4 доменов. Каждый дополнительный виджет — отдельный замер INP в PageSpeed Insights с throttling.

Partytown готов для production в Next.js 15?

Да, с experimental.nextScriptWorkers, но тестируйте все теги. Рекламные пиксели и GA4 обычно работают; чаты и A/B (Optimizely, VWO) — часто нет. Держите fallback без worker.

GTM в head через next/head — устарело?

В App Router нет next/head для произвольных script вне metadata API. Используйте next/script в layout или Client Component. Метаданные OG/JSON-LD — через export const metadata или generateMetadata, не путайте с маркетинговыми тегами.

Влияют ли third-party на SEO напрямую?

Не как отдельный фактор ранжирования «список доменов», но через Core Web Vitals и поведенческие сигналы (bounce, короткие сессии). Плохой INP на мобильных коррелирует с просадкой конверсии и ухудшением engagement в Search Console.

Заключение

Сторонние скрипты на SEO-лендинге Next.js 15 — не «вставить перед </body> и забыть». Стратегия lazyOnload для аналитики, отказ от тяжёлого GTM без необходимости, отложенный mount чата и Partytown для совместимых тегов держат INP в зелёной зоне вместе с уже оптимизированными LCP и CLS.

Зафиксируйте baseline в Search Console, подключите Speed Insights и договоритесь с маркетингом о changelog тегов. Тогда GA4 и пиксель конверсии остаются в отчётах, а лендинг не превращается в очередь long tasks на каждый клик по CTA.

KEL IT

Нужна разработка под ключ?

Я занимаюсь такими проектами профессионально. Telegram-боты, Mini Apps, сайты, мобильные и десктопные приложения. Расскажите о задаче — разберём и предложим решение.