Back

Sending Background Data with the Beacon API

Sending Background Data with the Beacon API

Page navigation shouldn’t be held hostage by analytics requests. When users click a link or close a tab, traditional HTTP requests can block or fail entirely, leaving you with incomplete data and frustrated users. The Beacon API solution changes this by queuing requests in the background, ensuring your data reaches the server without impacting performance.

This article demonstrates how to implement navigator.sendBeacon() for reliable background data transmission. You’ll learn practical applications like analytics tracking and error reporting, understand why it outperforms fetch() during page transitions, and see a complete implementation using modern JavaScript.

Key Takeaways

  • The Beacon API queues requests in the background without blocking page navigation
  • Use navigator.sendBeacon() for analytics, error reporting, and user action tracking
  • Beacons are more reliable than fetch() during page unload events
  • Keep payloads small and batch multiple events for optimal performance
  • Implement fallbacks for older browsers that don’t support the Beacon API
  • The API returns a boolean indicating successful queuing, not delivery confirmation

What Makes the Beacon API Different

The Beacon API approach solves a fundamental problem: traditional HTTP requests block page navigation. When you use fetch() or XMLHttpRequest during unload events, the browser must wait for the request to complete before allowing navigation to proceed. This creates a poor user experience and unreliable data transmission.

The Beacon API operates differently. It queues requests asynchronously and handles transmission in the background, even after the page has closed. The browser manages the entire process without blocking navigation or requiring your JavaScript to remain active.

Why Traditional Methods Fail During Page Transitions

Consider this common scenario using fetch():

window.addEventListener('beforeunload', () => {
  // This may fail or block navigation
  fetch('/analytics', {
    method: 'POST',
    body: JSON.stringify({ event: 'page_exit' })
  })
})

This approach has several problems:

  • The request may be cancelled when the page unloads
  • Navigation is blocked until the request completes
  • No guarantee the data will reach the server
  • Poor user experience with delayed page transitions

How navigator.sendBeacon() Works

The navigator.sendBeacon() method accepts two parameters: a URL endpoint and optional data. It returns a boolean indicating whether the request was successfully queued (not whether it reached the server).

const success = navigator.sendBeacon(url, data)

The browser handles the actual transmission, optimizing for network conditions and system resources. This fire-and-forget approach is perfect for analytics, logging, and diagnostic data where you don’t need a response.

Practical Implementation Examples

Basic Analytics Tracking

Here’s a minimal implementation for tracking user sessions:

// Track session data
const sessionData = {
  userId: 'user123',
  sessionDuration: Date.now() - sessionStart,
  pageViews: pageViewCount,
  timestamp: new Date().toISOString()
}

// Send data when page becomes hidden or user navigates away
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    const payload = JSON.stringify(sessionData)
    navigator.sendBeacon('/log', payload)
  }
})

window.addEventListener('beforeunload', () => {
  const payload = JSON.stringify(sessionData)
  navigator.sendBeacon('/log', payload)
})

Error Reporting System

The Beacon API excels at capturing and reporting JavaScript errors:

window.addEventListener('error', (event) => {
  const errorData = {
    message: event.message,
    filename: event.filename,
    line: event.lineno,
    column: event.colno,
    stack: event.error?.stack,
    userAgent: navigator.userAgent,
    timestamp: Date.now()
  }

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/log', JSON.stringify(errorData))
  }
})

User Action Tracking

Track specific user interactions without blocking the interface:

function trackUserAction(action, details) {
  const actionData = {
    action,
    details,
    timestamp: Date.now(),
    url: window.location.href
  }

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/log', JSON.stringify(actionData))
  }
}

// Usage examples
document.getElementById('cta-button').addEventListener('click', () => {
  trackUserAction('cta_click', { buttonId: 'cta-button' })
})

document.addEventListener('scroll', throttle(() => {
  const scrollPercent = Math.round(
    (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
  )
  trackUserAction('scroll', { percentage: scrollPercent })
}, 1000))

Complete Analytics Implementation

Here’s a comprehensive example combining multiple tracking scenarios:

class AnalyticsTracker {
  constructor(endpoint = '/log') {
    this.endpoint = endpoint
    this.sessionStart = Date.now()
    this.events = []
    
    this.setupEventListeners()
  }

  setupEventListeners() {
    // Track page visibility changes
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.sendBatch()
      }
    })

    // Track page unload
    window.addEventListener('beforeunload', () => {
      this.sendBatch()
    })

    // Track errors
    window.addEventListener('error', (event) => {
      this.trackEvent('error', {
        message: event.message,
        filename: event.filename,
        line: event.lineno
      })
    })
  }

  trackEvent(type, data) {
    this.events.push({
      type,
      data,
      timestamp: Date.now()
    })

    // Send batch if we have too many events
    if (this.events.length >= 10) {
      this.sendBatch()
    }
  }

  sendBatch() {
    if (this.events.length === 0) return

    const payload = {
      sessionId: this.generateSessionId(),
      sessionDuration: Date.now() - this.sessionStart,
      events: this.events,
      url: window.location.href,
      userAgent: navigator.userAgent
    }

    if (navigator.sendBeacon) {
      const success = navigator.sendBeacon(
        this.endpoint, 
        JSON.stringify(payload)
      )
      
      if (success) {
        this.events = [] // Clear sent events
      }
    }
  }

  generateSessionId() {
    return Math.random().toString(36).substring(2, 15)
  }
}

// Initialize tracker
const tracker = new AnalyticsTracker('/log')

// Track custom events
tracker.trackEvent('page_view', { page: window.location.pathname })

Browser Support and Fallbacks

The Beacon API has excellent browser support across modern browsers. For older browsers, implement a graceful fallback:

function sendData(url, data) {
  if (navigator.sendBeacon) {
    return navigator.sendBeacon(url, data)
  }
  
  // Fallback for older browsers
  try {
    const xhr = new XMLHttpRequest()
    xhr.open('POST', url, false) // Synchronous for unload events
    xhr.setRequestHeader('Content-Type', 'application/json')
    xhr.send(data)
    return true
  } catch (error) {
    console.warn('Failed to send data:', error)
    return false
  }
}

Best Practices for Beacon Implementation

Keep Payloads Small

The Beacon API is designed for small data packets. Limit payloads to essential information:

// Good: Focused, essential data
const essentialData = {
  event: 'conversion',
  value: 29.99,
  timestamp: Date.now()
}

// Avoid: Large, unnecessary data
const bloatedData = {
  event: 'conversion',
  value: 29.99,
  timestamp: Date.now(),
  entireDOMState: document.documentElement.outerHTML,
  allCookies: document.cookie,
  completeUserHistory: getUserHistory()
}

Batch Multiple Events

Instead of sending individual beacons for each event, batch them together:

const eventBatch = []

function addEvent(eventData) {
  eventBatch.push(eventData)
  
  // Send batch when it reaches a certain size
  if (eventBatch.length >= 5) {
    sendBatch()
  }
}

function sendBatch() {
  if (eventBatch.length > 0) {
    navigator.sendBeacon('/log', JSON.stringify(eventBatch))
    eventBatch.length = 0 // Clear the batch
  }
}

Handle Network Failures Gracefully

Since beacons don’t provide response feedback, implement client-side validation:

function validateAndSend(data) {
  // Validate data structure
  if (!data || typeof data !== 'object') {
    console.warn('Invalid data for beacon')
    return false
  }

  // Check payload size (browsers typically limit to 64KB)
  const payload = JSON.stringify(data)
  if (payload.length > 65536) {
    console.warn('Payload too large for beacon')
    return false
  }

  return navigator.sendBeacon('/log', payload)
}

Beacon API vs Fetch During Page Transitions

The key difference becomes apparent during page unload events:

// Beacon API: Non-blocking, reliable
window.addEventListener('beforeunload', () => {
  navigator.sendBeacon('/log', JSON.stringify(data)) // ✅ Reliable
})

// Fetch API: May block or fail
window.addEventListener('beforeunload', () => {
  fetch('/log', {
    method: 'POST',
    body: JSON.stringify(data),
    keepalive: true // Helps but doesn't guarantee success
  }) // ❌ May fail during navigation
})

The keepalive flag in fetch requests provides similar functionality but with less reliability than the Beacon API, which was specifically designed for this use case.

Conclusion

The Beacon API provides a robust solution for background data transmission without blocking page navigation. By queuing requests asynchronously, it ensures reliable data delivery while maintaining optimal user experience. Whether you’re implementing analytics tracking, error reporting, or user action logging, navigator.sendBeacon() offers the reliability and performance that traditional HTTP methods cannot match during page transitions.

The fire-and-forget nature of beacons makes them ideal for scenarios where you need to send data but don’t require a response. Combined with proper batching strategies and payload optimization, the Beacon API becomes an essential tool for modern web applications that prioritize both data collection and user experience.

FAQs

The Beacon API is a JavaScript web standard that sends small amounts of data to a server without waiting for a response. Unlike fetch or XMLHttpRequest, beacon requests are queued by the browser and sent asynchronously, making them perfect for analytics and logging during page navigation events where traditional requests might fail or block user experience.

No, the Beacon API is designed for small data packets, typically limited to 64KB by most browsers. If you need to send larger amounts of data, consider batching smaller requests or using alternative methods like the Fetch API with the keepalive flag for non-critical timing scenarios.

The Beacon API has excellent support in all modern browsers including Chrome, Firefox, Safari, and Edge. It does not work in Internet Explorer. For older browser support, implement a fallback using XMLHttpRequest or fetch, though these alternatives may not provide the same reliability during page unload events.

The navigator.sendBeacon method returns a boolean indicating whether the request was successfully queued by the browser, not whether it reached the server. Since beacons are fire-and-forget, you cannot access the server response or know definitively if the data was received. This is by design for optimal performance.

Use the Beacon API when you need to send data during page transitions, unload events, or when the page becomes hidden, and you do not need a response. Use fetch for regular API calls where you need to handle responses, errors, or when the timing is not critical to page navigation.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers