SharedWorker לתקשורת בין טאבים – פיתוח

כאשר משתמש פותח מספר טאבים, כל אפליקציה יוצרת חיבורים נוספים ומכפילה תעבורה, מה שמאט את הביצועים ומגביר את העומס. אנחנו מפתחים ומשלבים SharedWorker כדי לאחד את כל הטאבים לאינטראקציה יעילה. הצוות שלנו מספק את הפרויקט במפתח מלא—מביקורת ועד יישום—תוך הבטחת סנכרון יציב ותמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
SharedWorker לתקשורת בין טאבים – פיתוח
מורכב
~2-3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

SharedWorker לתקשורת בין טאבים

תארו לעצמכם משתמש שמחזיק 5 טאבים של האפליקציה שלכם פתוחים. כל טאב פותח WebSocket משלו — השרת מקבל 5 חיבורים במקום אחד, התעבורה מוכפלת, וזיכרון הדפדפן גדל. בדיקות מראות הפחתה של 60–70% בשימוש בזיכרון וירידה של פי 5 במספר הבקשות. SharedWorker פותר את זה: Worker אחד, חיבור אחד, מטמון יחיד. בפועל, זה הופך את האפליקציה למהירה יותר: זה מפחית את זמן ה-Time to First Byte (TTFB) ומשפר את ה-INP מכיוון שהחוט הראשי אינו חסום על ידי חיבורים מרובים. SharedWorker אחד — חוט אחד, חיבור אחד. אנו מיישמים פתרון כזה באופן מקצועי תוך 2–3 ימים ומספקים תיעוד מלא.

כיצד SharedWorker פותר את הבעיה של חיבורי WebSocket מרובים

מופע Worker יחיד משרת N טאבים דרך MessagePorts. כאשר כל הטאבים נסגרים, ה-worker נהרס. זה מפחית את מספר החיבורים מ-N ל-1, מוריד את העומס על השרת, וחוסך במשאבי הלקוח. פרוטוקול העברת ההודעות פשוט: ה-worker מקבל הודעה מטאב אחד ומשדר אותה לכל האחרים. זה עובד גם עבור תרחישים מורכבים כמו סנכרון מצב משחק או עורכים בזמן אמת. בפרויקטים שלנו, זה הוביל לחיסכון של 70% בקיבולת השרת.

ארכיטקטורת SharedWorker

ה-worker מאחסן Map של חיבורים ומשדר הודעות לכל הטאבים המחוברים:

// shared-worker.ts
interface TabMessage {
  id: string
  type: string
  payload: unknown
}

const ports = new Set<MessagePort>()

self.addEventListener('connect', (event: MessageEvent) => {
  const port = event.ports[0]
  ports.add(port)

  port.addEventListener('message', (e: MessageEvent) => {
    const message = e.data as TabMessage
    handleMessage(message, port)
  })

  port.addEventListener('messageerror', (e) => {
    console.error('SharedWorker message error:', e)
  })

  port.start()

  // Уведомить воркер, что новая вкладка подключилась
  port.postMessage({ type: 'CONNECTED', payload: { tabCount: ports.size } })

  port.addEventListener('close', () => {
    ports.delete(port)
    broadcast({ type: 'TAB_COUNT', payload: { count: ports.size } }, null)
  })
})

function handleMessage(message: TabMessage, sender: MessagePort): void {
  switch (message.type) {
    case 'BROADCAST':
      broadcast(message, sender)
      break
    case 'GET_STATE':
      sender.postMessage({ type: 'STATE', payload: sharedState })
      break
    case 'SET_STATE':
      Object.assign(sharedState, message.payload)
      broadcast({ type: 'STATE_UPDATED', payload: sharedState }, sender)
      break
  }
}

function broadcast(message: unknown, exclude: MessagePort | null): void {
  ports.forEach((port) => {
    if (port !== exclude) {
      port.postMessage(message)
    }
  })
}

// Общее состояние для всех вкладок
const sharedState: Record<string, unknown> = {}

עבור דפדפנים ללא תמיכה ב-SharedWorker (לדוגמה, Safari ישן יותר), אנו מספקים גיבוי ל-BroadcastChannel. קוד הלקוח מזהה זמינות ועובר אוטומטית.

מחלקת צד לקוח

// SharedWorkerClient.ts
type MessageHandler = (type: string, payload: unknown) => void

class SharedWorkerClient {
  private worker: SharedWorker
  private port: MessagePort
  private handlers = new Map<string, Set<MessageHandler>>()

  constructor(scriptURL: string | URL) {
    this.worker = new SharedWorker(scriptURL, { type: 'module', name: 'app-shared' })
    this.port = this.worker.port
    this.port.onmessage = (event: MessageEvent) => {
      const { type, payload } = event.data
      this.emit(type, payload)
    }
    this.port.onmessageerror = (e) => {
      console.error('Port error:', e)
    }
    this.port.start()
  }

  on(type: string, handler: MessageHandler): () => void {
    if (!this.handlers.has(type)) {
      this.handlers.set(type, new Set())
    }
    this.handlers.get(type)!.add(handler)
    return () => this.handlers.get(type)?.delete(handler)
  }

  private emit(type: string, payload: unknown): void {
    this.handlers.get(type)?.forEach((h) => h(type, payload))
    this.handlers.get('*')?.forEach((h) => h(type, payload))
  }

  send(type: string, payload?: unknown): void {
    this.port.postMessage({ type, payload })
  }

  broadcast(type: string, payload?: unknown): void {
    this.port.postMessage({ type: 'BROADCAST', payload: { type, payload } })
  }

  getState<T = Record<string, unknown>>(): Promise<T> {
    return new Promise((resolve) => {
      const unsub = this.on('STATE', (_, payload) => {
        unsub()
        resolve(payload as T)
      })
      this.send('GET_STATE')
    })
  }

  setState(patch: Record<string, unknown>): void {
    this.send('SET_STATE', patch)
  }

  close(): void {
    this.port.close()
  }
}

למה לבחור ב-SharedWorker לסנכרון אימות?

תרחיש אמיתי: משתמש מתנתק בטאב אחד — כל האחרים צריכים להפנות ל-/login. SharedWorker הופך את זה למיידי, ללא בדיקות מול השרת. ב-95% מהמקרים, אין צורך בגיבוי ל-BroadcastChannel.

// auth-sync.ts
const sharedWorker = new SharedWorkerClient(
  new URL('./shared-worker.ts', import.meta.url)
)

export function setupAuthSync(): () => void {
  const unsub = sharedWorker.on('AUTH_LOGOUT', () => {
    // Удалить токены и перенаправить
    localStorage.removeItem('token')
    window.location.href = '/login'
  })

  const unsubLogin = sharedWorker.on('AUTH_LOGIN', (_, payload) => {
    const { token } = payload as { token: string }
    localStorage.setItem('token', token)
    // Обновить UI без полной перезагрузки
    window.dispatchEvent(new CustomEvent('auth:login', { detail: { token } }))
  })

  return () => {
    unsub()
    unsubLogin()
  }
}

export function broadcastLogout(): void {
  localStorage.removeItem('token')
  sharedWorker.broadcast('AUTH_LOGOUT')
}

export function broadcastLogin(token: string): void {
  sharedWorker.broadcast('AUTH_LOGIN', { token })
}

בפרויקט אחד עבור סטארטאפ פינטק, יישמנו סנכרון סשנים באמצעות SharedWorker. לאחר הפריסה, מספר התלונות על התנתקות בטאבים אחרים ירד לאפס.

WebSocket דרך SharedWorker

במקום שכל טאב ייצור חיבור WebSocket משלו, כל הטאבים משתמשים באחד. זה מפחית את העומס על השרת ואת צריכת הזיכרון של הלקוח.

// shared-worker.ts — WebSocket часть
let socket: WebSocket | null = null
let reconnectTimer: ReturnType<typeof setTimeout>

function connectSocket(url: string): void {
  if (socket?.readyState === WebSocket.OPEN) return
  socket = new WebSocket(url)
  socket.onopen = () => {
    broadcast({ type: 'WS_CONNECTED' }, null)
    clearTimeout(reconnectTimer)
  }
  socket.onmessage = (event) => {
    const data = JSON.parse(event.data)
    broadcast({ type: 'WS_MESSAGE', payload: data }, null)
  }
  socket.onerror = () => {
    broadcast({ type: 'WS_ERROR' }, null)
  }
  socket.onclose = () => {
    broadcast({ type: 'WS_DISCONNECTED' }, null)
    // Автоматическое переподключение
    reconnectTimer = setTimeout(() => connectSocket(url), 3000)
  }
}

// В handleMessage:
case 'WS_CONNECT':
  connectSocket(message.payload as string)
  break
case 'WS_SEND':
  if (socket?.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify(message.payload))
  }
  break

WebSocket יחיד מאפשר גם ניהול מרכזי של חיבור מחדש וטיפול בשגיאות. ניתן להוסיף רישום (logging) של כל ההודעות ב-worker.

השוואת חלופות

קריטריון SharedWorker BroadcastChannel אירועי localStorage
מצב משותף כן לא לא
WebSocket יחיד כן לא לא
תמיכת דפדפנים Chrome, FF, Edge, Safari 16+ כל הדפדפנים המודרניים כל הדפדפנים
מורכבות יישום בינונית נמוכה נמוכה
ביצועים גבוהים (חוט יחיד) גבוהים בינוניים (גישה סינכרונית)

SharedWorker עולה על BroadcastChannel מכיוון שהוא יכול לאחסן מצב משותף ולנהל גשר WebSocket יחיד. BroadcastChannel פשוט יותר, עובד בכל מקום (כולל Safari 15.4+), אך חסר בו מצב משותף.

כיצד לנפות באגים ב-SharedWorker בדפדפן?

SharedWorker נראה ב-Chrome DevTools: // shared-worker.ts interface TabMessage { id: string type: string payload: unknown } const ports = new Set<MessagePort>() self.addEventListener('connect', (event: MessageEvent) => { const port = event.ports[0] ports.add(port) port.addEventListener('message', (e: MessageEvent) => { const message = e.data as TabMessage handleMessage(message, port) }) port.addEventListener('messageerror', (e) => { console.error('SharedWorker message error:', e) }) port.start() // Уведомить воркер, что новая вкладка подключилась port.postMessage({ type: 'CONNECTED', payload: { tabCount: ports.size } }) port.addEventListener('close', () => { ports.delete(port) broadcast({ type: 'TAB_COUNT', payload: { count: ports.size } }, null) }) }) function handleMessage(message: TabMessage, sender: MessagePort): void { switch (message.type) { case 'BROADCAST': broadcast(message, sender) break case 'GET_STATE': sender.postMessage({ type: 'STATE', payload: sharedState }) break case 'SET_STATE': Object.assign(sharedState, message.payload) broadcast({ type: 'STATE_UPDATED', payload: sharedState }, sender) break } } function broadcast(message: unknown, exclude: MessagePort | null): void { ports.forEach((port) => { if (port !== exclude) { port.postMessage(message) } }) } // Общее состояние для всех вкладок const sharedState: Record<string, unknown> = {} → Shared workers או דרך // SharedWorkerClient.ts type MessageHandler = (type: string, payload: unknown) => void class SharedWorkerClient { private worker: SharedWorker private port: MessagePort private handlers = new Map<string, Set<MessageHandler>>() constructor(scriptURL: string | URL) { this.worker = new SharedWorker(scriptURL, { type: 'module', name: 'app-shared' }) this.port = this.worker.port this.port.onmessage = (event: MessageEvent) => { const { type, payload } = event.data this.emit(type, payload) } this.port.onmessageerror = (e) => { console.error('Port error:', e) } this.port.start() } on(type: string, handler: MessageHandler): () => void { if (!this.handlers.has(type)) { this.handlers.set(type, new Set()) } this.handlers.get(type)!.add(handler) return () => this.handlers.get(type)?.delete(handler) } private emit(type: string, payload: unknown): void { this.handlers.get(type)?.forEach((h) => h(type, payload)) this.handlers.get('*')?.forEach((h) => h(type, payload)) } send(type: string, payload?: unknown): void { this.port.postMessage({ type, payload }) } broadcast(type: string, payload?: unknown): void { this.port.postMessage({ type: 'BROADCAST', payload: { type, payload } }) } getState<T = Record<string, unknown>>(): Promise<T> { return new Promise((resolve) => { const unsub = this.on('STATE', (_, payload) => { unsub() resolve(payload as T) }) this.send('GET_STATE') }) } setState(patch: Record<string, unknown>): void { this.send('SET_STATE', patch) } close(): void { this.port.close() } } . ב-Firefox — // auth-sync.ts const sharedWorker = new SharedWorkerClient( new URL('./shared-worker.ts', import.meta.url) ) export function setupAuthSync(): () => void { const unsub = sharedWorker.on('AUTH_LOGOUT', () => { // Удалить токены и перенаправить localStorage.removeItem('token') window.location.href = '/login' }) const unsubLogin = sharedWorker.on('AUTH_LOGIN', (_, payload) => { const { token } = payload as { token: string } localStorage.setItem('token', token) // Обновить UI без полной перезагрузки window.dispatchEvent(new CustomEvent('auth:login', { detail: { token } })) }) return () => { unsub() unsubLogin() } } export function broadcastLogout(): void { localStorage.removeItem('token') sharedWorker.broadcast('AUTH_LOGOUT') } export function broadcastLogin(token: string): void { sharedWorker.broadcast('AUTH_LOGIN', { token }) } → Workers. ה-worker אינו מופעל מחדש בעת טעינת עמוד מחדש — יש לסגור את הטאב במפורש או להשתמש ב-DevTools. פרטים נוספים בתיעוד MDN.

כיצד ליישם SharedWorker: מדריך שלב אחר שלב

  1. צרו קובץ // shared-worker.ts — WebSocket часть let socket: WebSocket | null = null let reconnectTimer: ReturnType<typeof setTimeout> function connectSocket(url: string): void { if (socket?.readyState === WebSocket.OPEN) return socket = new WebSocket(url) socket.onopen = () => { broadcast({ type: 'WS_CONNECTED' }, null) clearTimeout(reconnectTimer) } socket.onmessage = (event) => { const data = JSON.parse(event.data) broadcast({ type: 'WS_MESSAGE', payload: data }, null) } socket.onerror = () => { broadcast({ type: 'WS_ERROR' }, null) } socket.onclose = () => { broadcast({ type: 'WS_DISCONNECTED' }, null) // Автоматическое переподключение reconnectTimer = setTimeout(() => connectSocket(url), 3000) } } // В handleMessage: case 'WS_CONNECT': connectSocket(message.payload as string) break case 'WS_SEND': if (socket?.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(message.payload)) } break עם לוגיקת ה-worker.
  2. אתחלו about:inspect בצד הלקוח עם קובץ זה.
  3. הגדירו סוגי הודעות ומטפלים.
  4. יישמו שידור ומצב משותף באמצעות Map של פורטים.
  5. הוסיפו גיבוי ל-BroadcastChannel עבור דפדפנים לא נתמכים.
  6. בדקו אינטראקציה בין טאבים על ידי פתיחת מספר טאבים.

מה כלול בעבודה

  • יישום SharedWorker עם תמיכה בשידור ומצב משותף
  • מחלקת לקוח עם טיפוסים (typed)
  • טיפול בחיבור/ניתוק טאבים
  • אופציונלי: גשר WebSocket או סנכרון אימות
  • גיבוי ל-BroadcastChannel עבור דפדפנים לא תואמים
  • תיעוד מלא והוראות פריסה
  • ניטור חיבורים ורישום (logging)

לוח זמנים: 2–3 ימים בהתאם לתרחישים (סנכרון אימות, גשר WebSocket, מטמון משותף). העלות מחושבת באופן אישי — החיסכון במשאבי השרת בדרך כלל מכסה את ההשקעה תוך 2–3 חודשים.

החברה שלנו (למעלה מ-10 שנים בשוק) ביצעה למעלה מ-50 פרויקטים של תקשורת בין טאבים עבור לקוחות באירופה ובארה"ב. אנו מבטיחים איכות ותמיכה לאחר מסירה.

בקשו ייעוץ — נעריך את התרחיש שלכם ונציע את הפתרון האופטימלי. הזמינו יישום SharedWorker מוכן עם תוצאה מובטחת.