Skip to main content
Блогқа оралу
JavaScriptAnalyticsEvent DelegationServer ComponentsPerformance

Әр батырма үшін клиенттік компонент емес, бір ғана тыңдаушы

Компонентке кликтерді бақылауды қосқан сәтте-ақ ол JavaScript жеткізетін клиенттік компонентке айналады. Мұны сайттың барлық жерінде жасасаңыз, тек бақылаудың өзі бандліңізді ісіндіреді. Мінеки, платформада ежелден бар балама: делегацияланған бір тыңдаушы, декларативті data-атрибуттар және таза серверде рендерленген HTML күйінде қалатын компоненттер.

Жарияланды 2026 ж. 28 шілде10 мин оқу

Мінеки, кез келген дерлік кодта қайталанатын сценарий. Продукт-менеджер сізден батырмадағы кликтерді бақылауды сұрайды. Батырма — әдемі, статикалық, серверде рендерленген компонент. Сонда сіз onClick өңдеушісін қосасыз. onClick өңдеушісі болу үшін компонент енді браузерде орындалуы керек — сондықтан сіз "use client" қосасыз (немесе хук не орауышқа жүгінесіз). Компонент гидратацияланады. Ол JavaScript жеткізеді. Әрі мұның бәрін клиентте бірдеңе істеу үшін емес, аналитикаңызға бір ғана жол сыбырлау үшін жасады.

Енді мұны үлкен сайттағы әрбір бақыланатын батырмаға, картаға, сілтемеге және қойындыға көбейтіңіз. JavaScript бандліңіздің елеулі бөлігі тек кликтер туралы хабарлау үшін ғана бар. Бұл мақала балама туралы — браузерде 1990-жылдардан бері отырған және ешқандай фреймворкті қажет етпейтін балама туралы: оқиға делегациясы. Кішкентай бір тыңдаушы бүкіл бетті бақылайды; ал компоненттеріңіз қайтадан таза HTML болып қалады.

Ойланбай қосатын өңдеуші

Бұл рефлекс зиянсыз көрінеді. Сізде сілтеме бар, оны басқан кезді білгіңіз келеді, сондықтан өңдеушіні тіркейсіз:

cta-button.tsx
"use client"; // ← the moment analytics arrives, so does this line

import { track } from "@/lib/analytics";

export function CtaButton({ href, label, place }: Props) {
  return (
    <a
      href={href}
      onClick={() =>
        track("cta_clicked", { place, label })
      }
    >
      {label}
    </a>
  );
}

Бақылаудың бір ғана жолы — және бүкіл компонент санатын өзгертті. Ол енді сервер ағынмен жіберіп, ұмыта алатын статикалық белгілеу емес — ол onClick пайда болғанша клиент жүктеп, талдап, гидратациялауы тиіс интерактивті арал. Фреймворк бұл өңдеушінің тек қана жіберіп-ұмытатын телеметрия екенін біле алмайды; ол білетіні — бұл компонент клиентте тірі болуы керек.

Бұл рефлекс кодтың бойында жинақталып өсетін шығындарды алып жүреді:

  • Әрбір бақыланатын компонент гидратация шекарасына айналады — интерфейсті мүлде өзгертпейтін нәрсе үшін JavaScript жүктеледі, талданады және орындалады.
  • Бақылау логикасы жүздеген компонентке шашырап, олардың әрқайсысы аналитика SDK-сын импорттайды, сондықтан SDK көптеген бандлдерге түсіп қалады.
  • Аналитика кітапханасы сыни жолда жүктеліп, пайдаланушы шын мәнінде күтіп отырған рендерлеумен бәсекеге түседі.
  • "Біз қандай оқиғаларды және қайдан жіберіп жатырмыз?" деген сұрақ археология жобасына айналады — жауап бүкіл компонент ағашына шашырап жатыр.

Неге бұл дұрыс сезінеді

Мұны неге істейтінімізге адал болған жөн. Бірге орналастыру (co-location) — шынымен де жақсы түйсік: клик пен сол клик туралы жазып алғыңыз келетін нәрсе бір жерде тұрады, сондықтан бақылау шақыруын дәл сол жерге қою оқуға жеңіл әрі түсінуге оңай. Фреймворктер мұны нығайтады — onClick props кликке жауап берудің айқын әрі құжатталған тәсілі, ал ол қосымша гидратацияны да ілестіре әкеледі.

Бірақ бақылау ниетін бірге орналастыру бақылайтын кодты бірге орналастыруды талап етпейді. Декларацияны элементтің қасында сақтауға болады — "бұл батырма — CTA клик" — ал нақты тыңдау мүлде басқа жерде болады. Браузер мұны әрдайым істей алатын; біз тек компоненттер әр элементке өңдеуші қоюды «тегін» сезіндіргеннен кейін оған жүгінуді қойдық.

Оқиғалар қалқып шығады — бүкіл айла осында

Элементті басқанда браузер оқиғаны тек сол элементте ғана тудырмайды. Оқиға DOM ағашымен жоғары қарай жүреді — элементтен оның ата-анасына, одан оның ата-анасына, тіпті document-ке дейін. Бұл — қалқып шығу (bubbling), әрі ол ең жоғарыдағы жалғыз тыңдаушы астындағы бәрінің кликтерін бақылай алады дегенді білдіреді. Сізге әр батырмаға тыңдаушы қажет емес; сізге құжатты бақылайтын және әр клик сайын шын мәнінде не басылғанын анықтайтын бір тыңдаушы қажет.

Ол үшін құрал — Element.closest(). Клик түскен жерден бастап — ол сілтемеңіздің ішіне енгізілген белгіше немесе <span> болуы мүмкін — closest() селекторға сәйкес ата-тек элементін тапқанша жоғары қарай жүреді. Одан бақылау маркерін алып жүрген ең жақын элементті сұраңыз, сонда меңзердің дәл астында не болғанына қарамастан, басылған нақ сол батырманы қайтарып аласыз. Осы бір ғана примитив әрбір компоненттік өңдеушіні алмастырады.

Келісім — data-атрибут

Егер кликті ғаламдық тыңдаушы өңдейтін болса, компоненттің жалғыз міндеті — не бақылану керектігін жариялау, әрі мұны сервер рендерлей алатын белгілеуде жасау. HTML-де бұл механизм бұрыннан бар: data-* атрибуттар. Батырма өзін өзі сипаттайды және ешқандай мінез-құлық жеткізбейді:

cta-button.tsx
// No "use client". No handler. No hooks. Pure server-rendered HTML.
export function CtaButton({ href, label, place }: Props) {
  return (
    <a
      href={href}
      data-track="cta_clicked"
      data-place={place}
      data-label={label}
    >
      {label}
    </a>
  );
}

Мұнда "use client" жоқ, өңдеуші жоқ, импортталған SDK жоқ — клиент гидратациялайтын ештеңе жоқ. Сервер қарапайым HTML-ді ағынмен жібереді, ал бақылау ниеті атрибут ретінде қоса жүреді. Браузерге шын мәнінде жететіні мынау:

<!-- What the server sends. The browser needs nothing else to make it work. -->
<a href="/pricing" data-track="cta_clicked" data-place="hero" data-label="Start free">
  Start free
</a>

Бүкіл бетке бір тыңдаушы

Енді мінез-құлық дәл бір жерде тұрады. document-тегі жалғыз клик тыңдаушысы не басылса, сол маркерді оқып, ол туралы хабарлайды. Компоненттің data-track мәні оқиға атауына айналады; қалған data-* атрибуттар пайдалы жүкке (payload) айналады — dataset API оларды қарапайым объект ретінде береді:

track-delegation.ts
// The entire client-side cost of analytics for the whole site.
function handleClick(e: MouseEvent) {
  // Find the nearest tracked element from wherever the click landed —
  // works even if the user clicked an icon or <span> inside the link.
  const el = (e.target as HTMLElement | null)?.closest<HTMLElement>("[data-track]");
  if (!el) return;

  const { track: event, ...data } = el.dataset;
  send(event!, data);
}

export function registerTracking() {
  document.addEventListener("click", handleClick);
}

Оны бір рет, қосымша іске қосылған кезде, гидратацияға дейін тіркейсіз — сонда ол ең алғашқы бояудан бастап бақылап тұрады. Бұл шақыру қай жерде тұратыны стегіңізге байланысты, бірақ ол әрдайым жалғыз шақыру:

app entry (runs once, before hydration)
import { registerTracking } from "./track-delegation";

// Next.js: instrumentation-client.ts · Vite/SPA: main.ts · plain HTML: a <script>.
// One call, once, for the entire application.
registerTracking();

SDK-ны тек нақты клик болғанда ғана жүктеңіз

Мұнда нәзік, екінші ұтыс бар. Тыңдаушының өзі кішкентай — тәуелділіктері жоқ бірнеше жол. Аналитиканың ауыр бөлігі — SDK, ал делегациямен ол бетті жүктеу кезінде енді қажет емес. Оны импорттауды алғашқы клик шынымен болғанша кейінге қалдыруға болады:

track-delegation.ts
// The listener ships ~1 KB. The heavy SDK is pulled only when a real
// click happens — never during page load.
async function send(event: string, data: Record<string, string | undefined>) {
  const { track } = await import("@/lib/analytics"); // code-split, on demand
  track(event, data);
}

Енді аналитика провайдерінің килобайттары сыни жолдан толығымен шығып қалды. Бақылауға қатысты ешнәрсе алғашқы рендермен бәсекелеспейді; SDK жалқау түрде, сұраныс бойынша, пайдаланушы алғаш рет әрекет еткен сәтте келеді — ал баспай кететін пайдаланушы үшін ол мүлде жүктелмейді.

Тісін батыратын бөлшектер

Кликтерді делегациялаудың 90%-ын дұрыс жасап, содан кейін тәжірибелі пайдаланушыларға бөгет жасау оңай. Кәдімгі сол жақ клик — сіздің қолыңыздағы нәрсе, бірақ Cmd/Ctrl/Shift-клик немесе ортаңғы түйме клигі — пайдаланушының браузерден сілтемені жаңа қойындыда ашуды немесе жүктеп алуды сұрауы. Егер тыңдаушыңыз preventDefault()-ты шартсыз шақырса, сіз бұл ниетті үнсіз тартып аласыз. Түрлендірілген және негізгі емес кликтерде ерте шығып кетіп, зәкірдің төл href-іне өз ісін атқаруға мүмкіндік беріңіз:

function handleClick(e: MouseEvent) {
  // Cmd/Ctrl/Shift/Alt-click and middle-click express a native intent
  // (open in new tab, download). Let the browser handle those untouched.
  if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
    return;
  }
  // ...find [data-track], send event
}

Екінші тұзақ — уақыт. Егер клик бетті басқа жаққа алып кетсе, беттің жүктеуден шығуы әлі жолда келе жатқан аналитика сұрауын үзіп тастауы мүмкін — сонда өлшегіңіз келетін нақ сол кликтер жоғалып кету ықтималдығы ең жоғары кликтер болып шығады. navigator.sendBeacon дәл осы үшін бар: ол кішкентай пайдалы жүкті браузерге фонда жеткізуге береді, әрі навигациядан аман өтеді:

function send(event: string, data: Record<string, string | undefined>) {
  // A click that unloads the page can abort an in-flight fetch. sendBeacon
  // hands the request to the browser to deliver even as navigation happens.
  navigator.sendBeacon(
    "/track",
    JSON.stringify({ event, ...data }),
  );
}

Клиенттік маршрутизатор өңдейтін ішкі сілтемелерге бұның ешқайсысы қатысты емес — жүктеуден шығу болмайды, сондықтан қалыпты сұрау жарайды әрі делегацияланған тыңдаушы маршрутизатормен қатар тыныш отыра береді. Beacon шынайы, толық беттік навигацияны тудыратын сілтемелер үшін маңызды. Қай сілтемеңіз қайсысын жасайтынын білу — бүкіл нәзіктік осында.

Тыңдаушы сілтемені ешқашан бұзбауы тиіс. Түрлендірілген кликтерді құрметтеңіз, навигациялар үшін beacon қолданыңыз және аналитиканы қатаң түрде «мүмкіндігінше» деп қараңыз — бақылаудың сәтсіздігі пайдаланушыға көрінбеуі керек, ешқашан жансыз батырмаға айналмауы тиіс.

Кликтерден тыс

Кликтер — жиі кездесетін жағдай, бірақ бұл үлгі делегацияланған тыңдаушы бақылай алатын кез келген оқиғаға жалпыланады. Мысалы, төл <details> аккордеоны нөл JavaScript-пен ашылып-жабылады — әрі оны адамдар қаншалықты жиі ашатынын өлшей аласыз, өйткені toggle оқиғасын дәл осылай делегациялауға болады. Бір ғана қиындық: toggle қалқып шықпайды, сондықтан оны ұстау (capture) фазасында тыңдайсыз:

// The same pattern, a different event. A native <details> accordion needs
// zero JavaScript to open — and one delegated listener to be measurable.
document.addEventListener("toggle", (e) => {
  const el = e.target as HTMLElement;
  if (el.matches("[data-track-open]") && (el as HTMLDetailsElement).open) {
    send(el.dataset.trackOpen!, { ...el.dataset });
  }
}, true); // capture: the toggle event does not bubble

Форма жіберулері, көріну өзгерістері, медиа ойнатылуы — бәріне сол бір декларативті пішін қолданылады. Компонент белгілеуде нені жазып алу керегін мәлімдейді; ал жазып алуды бірнеше ғаламдық тыңдаушы атқарады. Бетіңіздің интерактивті беті мен бетіңіздің бақыланымдылығы енді бір нәрсе болуын тоқтатады.

Неге бұл қазір бұрынғыдан маңыздырақ

Оқиға делегациясына ондаған жыл болды, әрі ұзақ уақыт бойы ол «болса жақсы» нәрсе болатын — мыңның орнына бір тыңдаушыны тізімге тіркеу тәсілі. Server Components дәуірінде оның маңызы жоғарырақ. Әдепкі нұсқа — серверде рендерленетін және ешқандай JavaScript жеткізбейтін компонент болғанда, жалғыз onClick енді дөңгелектеу қатесі емес: ол бүкіл бөлімді статикалық HTML-ден гидратацияланған клиенттік аралға аударып тастайтын жол.

Осы әдепкіні сақтап қалуға мүмкіндік беретін нәрсе — делегация. Аналитика — тарихи тұрғыда презентациялық компонент клиенттік компонент болуға «мәжбүр» болатын ең жиі себептердің бірі — енді шекараны күштеп орнатуды тоқтатады. Тек бақылануы қажет, бірақ интерактивті болуы қажет емес бүкіл бөлімдер серверде рендерленген күйінде қалып, ештеңе жеткізбей алады. Интерфейсіңізді бақылау құны оны гидратациялау құнынан ажыратылады.

Делегация ма, әр компонент пе — шынын айтқанда

МәселеӘр компонентте өңдеушіБір делегацияланған тыңдаушы
Әр бақыланатын элементтегі JavaScriptЖеткізіледі (гидратация шекарасы)Жоқ — таза серверлік HTML
Аналитика SDKКөптеген бандлдерге импортталадыБір рет, жалқау түрде, алғашқы клик кезінде жүктеледі
Сыни жолдаИәЖоқ
Бақылау қайда орналасадыАғаш бойына шашылғанБір файл
Барлық оқиғаларды тексеруБүкіл кодты grep-теуМаркерлерді оқу / бір өңдеуші
Кейін қосылған элементтер үшін жұмыс істейдіӘрқайсысына өңдеуші керекАвтоматты түрде (делегация)
Фреймворкке байланыстылықКомпонент өмірлік циклімен байланыстыТаза DOM — фреймворктен тәуелсіз

Делегацияның адал бағасы — азғантай тәртіп: келісім енді типтелген функция шақыруының орнына жолмен типтелген атрибуттар, сондықтан data-track атауындағы қате компиляция кезінде емес, үнсіз істен шығады. Типтелген аргументтен атрибуттарды құратын кішкентай көмекші функция осы қауіпсіздіктің көбін қайтарып береді, ал бір ғана жақсы типтелген тыңдаушыны адал ұстау жүздеген файлға шашылған өңдеушілерге қарағанда әлдеқайда кіші бет.

Мұның ешқайсысы жаңалық емес

Егер бұл таныс сезілсе, солай болуы тиіс. Тег-менеджерлер мен автотіркеу (autocapture) аналитикасы дәл осылай әрдайым жұмыс істеп келді. Google Tag Manager, Segment, PostHog, Heap — олар астыртын бірнеше ғаламдық тыңдаушы тіркеп, не басылса, содан атрибуттарды оқиды (немесе селекторларды тұспалдап анықтайды). Сіз бұл үлгіні бұған дейін де жеткізгеніңіз анық; тек оны үшінші тарап скриптіне иелік етуге бердіңіз.

Мұны әдейі жасаудың мәні — бақылау мен салмақ. Шамамен отыз жол сізге сыни жолыңызда провайдер скрипті болмай-ақ, сіз көздемеген оқиғаларды тудыратын мөлдір емес автотіркеусіз, әрі оқи, типтей және тексере алатын data-атрибут келісімімен дәл сол делегация механикасын береді. Сіз автотіркеуді танымал еткен эргономиканы сақтап, салықтан құтыласыз.

Түйін

  • Презентациялық компонентке бақылаушы onClick қосу оны JavaScript жеткізетін клиенттік компонентке үнсіз көтереді.
  • Сізге әр элементке өңдеуші қажет емес — кликтер document-ке дейін қалқып шығады, ал closest("[data-track]") нақ не басылғанын қалпына келтіреді.
  • Компоненттер бақылауды data-* атрибуттарымен жарияласын, ал мінез-құлықты іске қосу кезінде бір рет тіркелген бір ғаламдық тыңдаушыда сақтаңыз.
  • Аналитика SDK-сын тыңдаушының ішінде жалқау импорттаңыз, сонда ол сыни жолдан тыс қалады әрі әрекеттеспейтін пайдаланушылар үшін мүлде жүктелмейді.
  • Түрлендірілген/негізгі емес кликтерді құрметтеңіз, навигациялар үшін sendBeacon қолданыңыз және аналитиканы «мүмкіндігінше» деңгейінде ұстаңыз, сонда ол сілтемені ешқашан бұза алмайды.
  • Server Components дәуірінде делегация — аналитиканың статикалық бөлімдерді гидратацияланған аралдарға айналдыруын тоқтататын нәрсе.

Мұндағы бөліктердің ешқайсысы экзотикалық емес: оқиғаның қалқып шығуы, closest(), data-* атрибуттары, жалқау import() және sendBeacon — бәрі де қарапайым, жақсы қолдау көрсетілген платформа мүмкіндіктері. Өзгеріс — мінез-құлықты қайда орналастыратыныңызда. Әр компонентпен бірге клик өңдеушісін жеткізуді тоқтатыңыз, бүкіл бетке бір тыңдаушы жеткізіңіз, әрі компоненттеріңізге өздері ең жақсы рендерлейтін нәрсе — HTML болып қайта оралуға мүмкіндік беріңіз.