Сторонние скрипты на лендинге 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) измеряет задержку от действия пользователя до следующего кадра. Сторонние скрипты ухудшают его тремя путями:
- Long tasks — синхронный парсинг и выполнение JS в main thread при загрузке и после idle.
- Event listeners — глобальные обработчики
click,scroll,touchstartс дорогой логикой (heatmap, session replay). - Конкуренция за сеть — скрипты в
<head>безdeferоткладывают discovery hero и шрифтов.
LCP страдает реже, но тоже: render-blocking script в <head>, синхронный GTM snippet, или чат-виджет, который вставляет iframe above-the-fold до отрисовки hero.
| Сервис | Типичный вес (gzip) | Риск для CWV |
|---|---|---|
| Google Analytics 4 (gtag) | ~45–90 KB | INP, long tasks |
| Google Tag Manager | контейнер + теги | LCP, INP |
| Meta Pixel | ~30–80 KB | INP, privacy |
| Intercom / Crisp / Carrot | 150–400 KB+ | INP, LCP (launcher) |
| Hotjar / Clarity | 80–200 KB | INP (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 | После load | GA4, пиксели, большинство тегов |
worker | Web 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:
- Предпочитайте прямой gtag вместо GTM, если тегов ≤ 3 (GA4 + один Ads + один Pixel).
- Если GTM обязателен — один контейнер,
strategy="lazyOnload", триггеры только наDOM Ready/Window Loaded, не наAll Pages+ custom HTML sync. - Запретите в GTM Custom HTML с
<script>без defer и теги «Facebook Pixel» + «GA4» + «LinkedIn» на одной странице без приоритизации. - Включите 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>
);
}
Процесс в команде:
- Baseline — зафиксируйте INP/LCP в Search Console до подключения GTM.
- Change log — каждый новый тег в GTM = запись в Notion + дата; через 28 дней сравните CrUX.
- Lab gate — Lighthouse CI на PR: порог «Total Blocking Time» и «Script Evaluation»; падение блокирует merge.
- Сегментация — 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.