ווידג'ט של צד שלישי מכניס אלמנטים באופן אסינכרוני, והלוגיקה שלך לא מופעלת? או שאתה צריך לעקוב אחר תוכן שמתווסף דינמית ללא setInterval? MutationObserver — ממשק API מקורי של הדפדפן למעקב ראקטיבי אחר שינויים ב-DOM. השתמשנו בו ביותר מ-50 פרויקטים לשילוב קוד legacy, עורכי CMS ואנליטיקה. בעיות טיפוסיות: שינויים שהוחמצו, דליפות זיכרון, טריגרים מיותרים לפריסה. הגדרה נכונה של ה-observer עם סינון וניתוק בזמן פותרת אותן. חיסכון בזמן דיבאג מגיע עד 40%, ועלויות התחזוקה יורדות ב-25%. אם אתה צריך יישום — צור קשר לייעוץ. המהנדסים שלנו מוכנים ליישם MutationObserver במפתח מלא תוך 1–2 ימים.
בעיות עיקריות ש-MutationObserver פותר
- אינטגרציה עם קוד legacy שבו אין לך גישה לקוד המקור או שאי אפשר לשכתב לוגיקה קיימת.
- מעקב אחר תוכן שמוכנס דינמית: ווידג'טים לתמיכה, באנרים לפרסום, צ'אטים — כל מה שמופיע לאחר טעינת הדף.
- אנליטיקת שינויים בדף ללא כלים חיצוניים: איסוף נתוני התנהגות משתמשים, בדיקות A/B.
- יישום אלמנטים מותאמים אישית ללא Web Components API: למשל, אתחול אוטומטי של tooltips או modals.
כיצד MutationObserver פותר את בעיית הווידג'ט האסינכרוני
לאחרונה שילבנו את Intercom בדף נחיתה. הווידג'ט המוגדר כברירת מחדל החיל סגנונות מתנגשים. השתמשנו בפונקציה waitForElement כדי לחכות להופעת קונטיינר הווידג'ט ולעקוף את הסגנונות מיד לאחר הוספתו. זה לקח שעתיים לעומת יומיים אם היינו משתמשים בבדיקות תקופתיות כל 100ms.
למה MutationObserver מהיר יותר מבדיקות תקופתיות
השוואת מאפיינים:
| מאפיין | MutationObserver | בדיקות תקופתיות (setInterval 100ms) |
|---|---|---|
| השהיית תגובה | מיקרוטאסק, כמעט אפס | מינימום 100ms |
| עומס CPU | רק בעת שינויים | קבוע, 10 בדיקות/שנייה |
| צריכת זיכרון | מינימלית | טיימרים מרובים |
| מורכבות יישום | בינונית, דורש ידע ב-API | פשוטה מאוד |
MutationObserver עולה על בדיקות תקופתיות פי 5–10 בזמן שינויים פעילים ב-DOM. המדידות שלנו הראו ירידה של 30% בזמן תגובת הממשק לאחר החלפת בדיקות תקופתיות ב-MutationObserver.
הגדרה בסיסית ומדריך שלב אחר שלב
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
switch (mutation.type) {
case 'childList':
// mutation.addedNodes — добавленные узлы (NodeList)
// mutation.removedNodes — удалённые узлы
break
case 'attributes':
// mutation.attributeName — имя атрибута
// mutation.oldValue — старое значение (если attributeOldValue: true)
break
case 'characterData':
// mutation.oldValue — старый текст (если characterDataOldValue: true)
break
}
}
})
observer.observe(element, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'data-state'],
attributeOldValue: true,
characterData: false,
})
observer.disconnect()
observer.takeRecords()
שלבים:
- צור מופע
const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { switch (mutation.type) { case 'childList': // mutation.addedNodes — добавленные узлы (NodeList) // mutation.removedNodes — удалённые узлы break case 'attributes': // mutation.attributeName — имя атрибута // mutation.oldValue — старое значение (если attributeOldValue: true) break case 'characterData': // mutation.oldValue — старый текст (если characterDataOldValue: true) break } } }) observer.observe(element, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-state'], attributeOldValue: true, characterData: false, }) observer.disconnect() observer.takeRecords(). הקולבק מקבל מערך של מוטציות. - קרא ל-
new MutationObserver(callback)— ציין את אלמנט היעד וההגדרות: לפחות דגל אחד:observe(target, options),childList, אוattributes. - עבד את המוטציות בתוך הקולבק: בדוק את
characterDataוחלץ נתונים מ-mutation.type,addedNodesוכו'. - נתק את ה-observer באמצעות
attributeNameכאשר אין עוד צורך במעקב. השתמש ב-disconnect()לפני הניתוק כדי לטפל במוטציות שנותרו.
| פרמטר | סוג | תיאור |
|---|---|---|
| childList | boolean | מעקב אחר הוספה/הסרה של צמתי ילדים |
| attributes | boolean | מעקב אחר שינויי תכונות |
| characterData | boolean | מעקב אחר שינויי תוכן טקסט |
| subtree | boolean | מעקב אחר כל הצאצאים (כולל עמוקים) |
| attributeFilter | string[] | סינון תכונות למעקב |
| attributeOldValue | boolean | שמירת ערך תכונה ישן |
| characterDataOldValue | boolean | שמירת תוכן טקסט ישן |
דוגמאות שימוש מעשיות
המתנה להופעת אלמנט ב-DOM שימושי עבור ווידג'טים של צד שלישי שמכניסים אלמנטים באופן אסינכרוני:
function waitForElement<T extends HTMLElement>(
selector: string,
root: HTMLElement | Document = document,
timeoutMs = 10000
): Promise<T> {
const existing = root.querySelector<T>(selector)
if (existing) return Promise.resolve(existing)
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
observer.disconnect()
reject(new Error(`Элемент "${selector}" не появился за ${timeoutMs}ms`))
}, timeoutMs)
const observer = new MutationObserver(() => {
const el = root.querySelector<T>(selector)
if (el) {
clearTimeout(timer)
observer.disconnect()
resolve(el)
}
})
observer.observe(root, { childList: true, subtree: true })
})
}
// Использование:
const chatWidget = await waitForElement<HTMLDivElement>('#intercom-container')
chatWidget.style.bottom = '80px'
מעקב אחר אלמנטים שמתווספים דינמית למקרים שבהם צריך לאתחל לוגיקה עבור אלמנטים שעשויים להופיע בכל רגע:
type ElementHandler = (element: HTMLElement) => (() => void) | void
function watchForElements(
selector: string,
handler: ElementHandler,
root: HTMLElement | Document = document
): () => void {
const cleanups = new Map<HTMLElement, () => void>()
function processElement(el: HTMLElement): void {
if (cleanups.has(el)) return
const cleanup = handler(el)
if (cleanup) cleanups.set(el, cleanup)
}
function processRemoval(el: HTMLElement): void {
const cleanup = cleanups.get(el)
if (cleanup) {
cleanup()
cleanups.delete(el)
}
}
root.querySelectorAll<HTMLElement>(selector).forEach(processElement)
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return
const el = node as HTMLElement
if (el.matches(selector)) processElement(el)
el.querySelectorAll<HTMLElement>(selector).forEach(processElement)
})
mutation.removedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return
const el = node as HTMLElement
if (el.matches(selector)) processRemoval(el)
el.querySelectorAll<HTMLElement>(selector).forEach(processRemoval)
})
}
})
observer.observe(root, { childList: true, subtree: true })
return () => {
observer.disconnect()
cleanups.forEach((cleanup) => cleanup())
cleanups.clear()
}
}
// Пример: автоматически инициализировать кастомные компоненты
const stop = watchForElements('[data-tooltip]', (el) => {
const tooltip = new TooltipController(el)
return () => tooltip.destroy()
})מעקב אחר שינויי תכונות
function watchAttribute(
element: HTMLElement,
attribute: string,
onChange: (newValue: string | null, oldValue: string | null) => void
): () => void {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === attribute) {
onChange(
element.getAttribute(attribute),
mutation.oldValue
)
}
}
})
observer.observe(element, {
attributes: true,
attributeFilter: [attribute],
attributeOldValue: true,
})
return () => observer.disconnect()
}
// Синхронизация с классом стороннего компонента
watchAttribute(someWidget, 'class', (newValue, oldValue) => {
const wasOpen = oldValue?.includes('is-open')
const isOpen = newValue?.includes('is-open')
if (!wasOpen && isOpen) onWidgetOpen()
if (wasOpen && !isOpen) onWidgetClose()
}) React Hook עבור MutationObserver
function useMutationObserver(
target: HTMLElement | null,
callback: MutationCallback,
options: MutationObserverInit
): void {
const callbackRef = useRef(callback)
callbackRef.current = callback
useEffect(() => {
if (!target) return
const observer = new MutationObserver((...args) => callbackRef.current(...args))
observer.observe(target, options)
return () => observer.disconnect()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target, JSON.stringify(options)])
}
// Использование:
function DynamicContent() {
const containerRef = useRef<HTMLDivElement>(null)
const [childCount, setChildCount] = useState(0)
useMutationObserver(
containerRef.current,
(mutations) => {
setChildCount(containerRef.current?.childElementCount ?? 0)
},
{ childList: true }
)
return <div ref={containerRef}>{/* динамическое содержимое */}</div>
} טעויות נפוצות וביצועים
- אל תשתמש ב-
takeRecords()שלא לצורך — זו האפשרות היקרה ביותר. אם אתה צריך רק לעקוב אחר ילדים ישירים, השתמש ב-function waitForElement<T extends HTMLElement>( selector: string, root: HTMLElement | Document = document, timeoutMs = 10000 ): Promise<T> { const existing = root.querySelector<T>(selector) if (existing) return Promise.resolve(existing) return new Promise((resolve, reject) => { const timer = setTimeout(() => { observer.disconnect() reject(new Error(`Элемент "${selector}" не появился за ${timeoutMs}ms`)) }, timeoutMs) const observer = new MutationObserver(() => { const el = root.querySelector<T>(selector) if (el) { clearTimeout(timer) observer.disconnect() resolve(el) } }) observer.observe(root, { childList: true, subtree: true }) }) } // Использование: const chatWidget = await waitForElement<HTMLDivElement>('#intercom-container') chatWidget.style.bottom = '80px'בלבד. - שכחת
type ElementHandler = (element: HTMLElement) => (() => void) | void function watchForElements( selector: string, handler: ElementHandler, root: HTMLElement | Document = document ): () => void { const cleanups = new Map<HTMLElement, () => void>() function processElement(el: HTMLElement): void { if (cleanups.has(el)) return const cleanup = handler(el) if (cleanup) cleanups.set(el, cleanup) } function processRemoval(el: HTMLElement): void { const cleanup = cleanups.get(el) if (cleanup) { cleanup() cleanups.delete(el) } } root.querySelectorAll<HTMLElement>(selector).forEach(processElement) const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { mutation.addedNodes.forEach((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return const el = node as HTMLElement if (el.matches(selector)) processElement(el) el.querySelectorAll<HTMLElement>(selector).forEach(processElement) }) mutation.removedNodes.forEach((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return const el = node as HTMLElement if (el.matches(selector)) processRemoval(el) el.querySelectorAll<HTMLElement>(selector).forEach(processRemoval) }) } }) observer.observe(root, { childList: true, subtree: true }) return () => { observer.disconnect() cleanups.forEach((cleanup) => cleanup()) cleanups.clear() } } // Пример: автоматически инициализировать кастомные компоненты const stop = watchForElements('[data-tooltip]', (el) => { const tooltip = new TooltipController(el) return () => tooltip.destroy() })בעת פירוק רכיב מובילה לדליפות זיכרון. תמיד החזר פונקציית ניקוי מ-function watchAttribute( element: HTMLElement, attribute: string, onChange: (newValue: string | null, oldValue: string | null) => void ): () => void { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === attribute) { onChange( element.getAttribute(attribute), mutation.oldValue ) } } }) observer.observe(element, { attributes: true, attributeFilter: [attribute], attributeOldValue: true, }) return () => observer.disconnect() } // Синхронизация с классом стороннего компонента watchAttribute(someWidget, 'class', (newValue, oldValue) => { const wasOpen = oldValue?.includes('is-open') const isOpen = newValue?.includes('is-open') if (!wasOpen && isOpen) onWidgetOpen() if (wasOpen && !isOpen) onWidgetClose() }). - גישה ל-DOM בתוך הקולבק שלא לצורך — כל querySelector גורם לפריסה כפויה. השתמש בנתוני המוטציה במקום זאת.
- אי קריאה ל-
function useMutationObserver( target: HTMLElement | null, callback: MutationCallback, options: MutationObserverInit ): void { const callbackRef = useRef(callback) callbackRef.current = callback useEffect(() => { if (!target) return const observer = new MutationObserver((...args) => callbackRef.current(...args)) observer.observe(target, options) return () => observer.disconnect() // eslint-disable-next-line react-hooks/exhaustive-deps }, [target, JSON.stringify(options)]) } // Использование: function DynamicContent() { const containerRef = useRef<HTMLDivElement>(null) const [childCount, setChildCount] = useState(0) useMutationObserver( containerRef.current, (mutations) => { setChildCount(containerRef.current?.childElementCount ?? 0) }, { childList: true } ) return <div ref={containerRef}>{/* динамическое содержимое */}</div> }לפניsubtree: true— מוטציות שלא עובדו אובדות.
ביצועים: MutationObserver יכול לצבור אלפי מוטציות בשנייה. סנן מוטציות במהירות, השתמש ב-childList: true, הימנע מפעולות כבדות בתוך הקולבק, ודחה אותן ל-requestAnimationFrame או ל-Web Worker.
השירותים שלנו ושלבי היישום
- הגדרת MutationObserver מותאמת לתרחישי הפרויקט שלך.
- פונקציות מוכנות לשימוש (waitForElement, watchForElements, React hooks) מותאמות לסטאק שלך.
- אינטגרציה עם רכיבי React/Vue.
- תיעוד וסקירת קוד.
- אחריות ללא דליפות זיכרון או רגרסיות.
תהליך:
- ניתוח דרישות — שעה אחת. קביעה אילו אלמנטים לעקוב וכיצד להגיב.
- פיתוח ובדיקות — 0.5–1 יום. כתיבת קוד, כיסוי בבדיקות.
- סקירת קוד ופריסה — 2–4 שעות. בדיקת איכות, פריסה לייצור.
- תמיכה — שבועיים לאחר המסירה. מענה על שאלות, תיקון באגים.
מה אנחנו מספקים
הפתרון שלנו במפתח מלא כולל:
- קוד מקור מלא עם דוגמאות למקרי השימוש הספציפיים שלך.
- תיעוד על הגדרה ופתרון בעיות.
- גישה ל-repository הפרטי שלנו ב-GitHub לניהול גרסאות.
- שבועיים של תמיכה לאחר המסירה עם זמן תגובה של 24 שעות.
- אופציונלי: סשן הדרכה למפתחים שלך (שעה אונליין).
לוחות זמנים ומחירים
לוחות זמנים: בין יום ל-3 ימים בהתאם למורכבות התרחיש. המחיר מתחיל מ-$500 לאינטגרציה פשוטה. פרויקטים מורכבים יותר עם observers מותאמים אישית ואלמנטים מרובים עולים בממוצע $1,500. לקוחות משיגים חיסכון של 25% בעלויות התחזוקה ועד $5,000 בשנה בזמן דיבאג מופחת. קבל הערכה לפרויקט שלך — שלח בקשה.
היתרונות שלנו
השתמשנו ב-MutationObserver ביותר מ-50 פרויקטים במשך שנים רבות של ניסיון. כל הפתרונות עוברים סקירת קוד ובדיקות. אנחנו מבטיחים ללא רגרסיות ומספקים תיעוד. המהנדסים שלנו הם מומחים מוסמכים עם ניסיון רב.
למידע נוסף על ה-API ב-MDN.







