Performance Optimization#

Pulsora is engineered to have minimal impact on your site's performance. This guide shows you how to optimize Pulsora integration for the best user experience.

Performance Metrics#

Bundle Size Impact#

Package Minified Gzipped Brotli
@pulsora/core 8.5 KB 2.99 KB 2.7 KB
@pulsora/react 2.1 KB 743 B 650 B
@pulsora/revenue Server-only 0 KB 0 KB
Total (Core + React) 10.6 KB 3.7 KB 3.35 KB

Comparison with alternatives:

  • Google Analytics: ~45 KB gzipped
  • Plausible: ~1.5 KB gzipped (but less features)
  • Mixpanel: ~60 KB gzipped
  • Pulsora: 3.7 KB gzipped (full features)

Web Vitals Impact#

With proper integration, Pulsora has zero negative impact on Core Web Vitals:

  • LCP (Largest Contentful Paint): No impact (async loading)
  • FID (First Input Delay): <1ms (non-blocking)
  • CLS (Cumulative Layout Shift): 0 (no visual elements)
  • INP (Interaction to Next Paint): <50ms
  • TTFB (Time to First Byte): No impact (client-side only)

Bundle Size#

  • @pulsora/core — ~3 KB gzipped
  • @pulsora/react — <1 KB gzipped (excluding peer deps)
  • @pulsora/revenue — Server-only package; no impact on frontend bundles

Use tree shaking and modern bundlers to keep the footprint minimal. The CDN build is minified and served with long-lived caching headers.

Loading Strategy#

  • The CDN script loads asynchronously by default (async attribute).
  • SPA auto-tracking runs after DOMContentLoaded so it never blocks rendering.
  • navigator.sendBeacon is used to flush events without delaying unload navigation.

Customizing Auto Pageviews#

Disable automatic pageviews if you prefer manual control (e.g., analytics-heavy dashboards):

const pulsora = new Pulsora();
pulsora.init({
  apiToken: 'pub_123',
  autoPageviews: false,
});

Call pulsora.pageview() only when a meaningful route loads.

Batched Network Requests#

  • Events are sent immediately but retried with exponential backoff when offline.
  • Identify events use fetch to guarantee delivery (beacon APIs drop headers).
  • Revenue events are queued on the server; your request returns 202 Accepted.

Caching Fingerprints#

Fingerprints are generated once per session and cached in-memory. Heavy operations (canvas, WebGL) run during idle time to avoid jank.

CSR vs SSR#

  • Keep Pulsora initialization inside client-only modules to avoid hydration warnings.
  • For Next.js, wrap analytics in a 'use client' provider (see SSR guide).

Content-Security Policy (CSP)#

If you apply a strict CSP, allow the Pulsora endpoint/domain:

script-src 'self' https://cdn.pulsora.co;
connect-src 'self' https://pulsora.co;

Optimization Strategies#

1. Lazy Loading#

Load Pulsora only when needed, after critical resources:

Basic lazy loading:

// Defer loading until page is interactive
window.addEventListener('load', async () => {
  const { Pulsora } = await import('@pulsora/core');

  const pulsora = new Pulsora();
  pulsora.init({ apiToken: 'pub_xxx' });
});

Load on user interaction:

let pulsora = null;

async function initAnalytics() {
  if (pulsora) return pulsora;

  const { Pulsora } = await import('@pulsora/core');
  pulsora = new Pulsora();
  await pulsora.init({ apiToken: 'pub_xxx' });

  return pulsora;
}

// Initialize on first interaction
document.addEventListener('click', initAnalytics, { once: true });
document.addEventListener('scroll', initAnalytics, { once: true });

Next.js dynamic import:

// components/Analytics.tsx
'use client';

import dynamic from 'next/dynamic';

const PulsoraProvider = dynamic(
  () => import('@pulsora/react').then(mod => mod.PulsoraProvider),
  {
    ssr: false,
    loading: () => null
  }
);

export function Analytics({ children }) {
  return (
    <PulsoraProvider config={{ apiToken: process.env.NEXT_PUBLIC_PULSORA_TOKEN! }}>
      {children}
    </PulsoraProvider>
  );
}

2. Code Splitting#

Split analytics code from main bundle:

Webpack configuration:

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        analytics: {
          test: /[\\/]node_modules[\\/]@pulsora[\\/]/,
          name: 'analytics',
          chunks: 'async',
        },
      },
    },
  },
};

Vite configuration:

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          analytics: ['@pulsora/core', '@pulsora/react'],
        },
      },
    },
  },
};

3. Tree Shaking#

Import only what you need:

// ❌ Bad - imports entire package
import * as Pulsora from '@pulsora/core';

// ✅ Good - tree-shakeable
import { Pulsora } from '@pulsora/core';

// ✅ Even better - named imports only
import { Pulsora, type PulsoraConfig } from '@pulsora/core';

4. Deferred Initialization#

Initialize after critical app logic:

// app/layout.tsx
'use client';

import { PulsoraProvider } from '@pulsora/react';
import { useEffect, useState } from 'react';

export function RootLayout({ children }) {
  const [analyticsReady, setAnalyticsReady] = useState(false);

  useEffect(() => {
    // Wait for critical resources to load
    const timer = setTimeout(() => setAnalyticsReady(true), 2000);
    return () => clearTimeout(timer);
  }, []);

  return (
    <html>
      <body>
        {analyticsReady ? (
          <PulsoraProvider config={{ apiToken: process.env.NEXT_PUBLIC_PULSORA_TOKEN! }}>
            {children}
          </PulsoraProvider>
        ) : (
          children
        )}
      </body>
    </html>
  );
}

5. Event Batching#

Batch multiple events to reduce network requests:

class EventBatcher {
  constructor(pulsora, batchSize = 5, flushInterval = 5000) {
    this.pulsora = pulsora;
    this.queue = [];
    this.batchSize = batchSize;

    // Auto-flush every N seconds
    setInterval(() => this.flush(), flushInterval);
  }

  track(eventName, properties) {
    this.queue.push({ eventName, properties });

    if (this.queue.length >= this.batchSize) {
      this.flush();
    }
  }

  flush() {
    if (this.queue.length === 0) return;

    // Send all queued events
    this.queue.forEach(({ eventName, properties }) => {
      this.pulsora.event(eventName, properties);
    });

    this.queue = [];
  }
}

// Usage
const batcher = new EventBatcher(pulsora);
batcher.track('button_click', { button: 'cta' });
batcher.track('form_focus', { field: 'email' });
// Events sent together after 5 items or 5 seconds

6. Conditional Loading#

Only load analytics in production:

// Only initialize in production
if (process.env.NODE_ENV === 'production') {
  const pulsora = new Pulsora();
  pulsora.init({ apiToken: process.env.PULSORA_TOKEN });
}

// Or use environment variable
if (process.env.ENABLE_ANALYTICS === 'true') {
  // Initialize analytics
}

Real-World Performance Case Studies#

Case Study 1: E-commerce Site#

Before Pulsora:

  • Lighthouse Performance Score: 88
  • LCP: 2.1s
  • FCP: 1.4s
  • Bundle size: 245 KB

After Pulsora (optimized):

  • Lighthouse Performance Score: 89 (+1 point)
  • LCP: 2.0s (-0.1s)
  • FCP: 1.3s (-0.1s)
  • Bundle size: 248.7 KB (+3.7 KB)

Optimization used: Dynamic import with requestIdleCallback

requestIdleCallback(() => {
  import('@pulsora/core').then(({ Pulsora }) => {
    const pulsora = new Pulsora();
    pulsora.init({ apiToken: 'pub_xxx' });
  });
});

Case Study 2: SaaS Dashboard#

Before Pulsora:

  • Time to Interactive: 3.2s
  • Total Blocking Time: 180ms
  • Bundle size: 512 KB

After Pulsora (with code splitting):

  • Time to Interactive: 3.2s (no change)
  • Total Blocking Time: 182ms (+2ms)
  • Bundle size: 515.7 KB (+3.7 KB)

Optimization used: Webpack code splitting + lazy loading

Case Study 3: Content Website#

Before Pulsora:

  • Lighthouse Performance Score: 95
  • LCP: 1.2s
  • CLS: 0.02

After Pulsora (CDN):

  • Lighthouse Performance Score: 95 (no change)
  • LCP: 1.2s (no change)
  • CLS: 0.02 (no change)

Optimization used: Async CDN script tag

<script
  async
  src="https://cdn.pulsora.co/v1/pulsora.min.js"
  data-token="pub_xxx"
></script>

Testing Performance Impact#

Lighthouse Audit#

# Run Lighthouse audit before
npx lighthouse https://yoursite.com --view

# Add Pulsora

# Run Lighthouse audit after
npx lighthouse https://yoursite.com --view

# Compare Performance scores

WebPageTest#

  1. Visit webpagetest.org
  2. Test your site before adding Pulsora
  3. Add Pulsora with optimization
  4. Test again and compare:
    • Start Render
    • Speed Index
    • Total Blocking Time
    • Request count

Chrome DevTools Performance#

// Measure Pulsora initialization time
performance.mark('pulsora-start');

const pulsora = new Pulsora();
await pulsora.init({ apiToken: 'pub_xxx' });

performance.mark('pulsora-end');
performance.measure('pulsora-init', 'pulsora-start', 'pulsora-end');

// Check measurement
const [measure] = performance.getEntriesByName('pulsora-init');
console.log(`Pulsora init: ${measure.duration.toFixed(2)}ms`);
// Should be <100ms typically

Bundle Analyzer#

Webpack Bundle Analyzer:

npm install --save-dev webpack-bundle-analyzer

# webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

npm run build
# Open generated report to verify Pulsora size

Next.js Bundle Analyzer:

npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
});

module.exports = withBundleAnalyzer({});

# Run analysis
ANALYZE=true npm run build

Performance Monitoring#

Runtime Performance Tracking#

// Monitor analytics performance in production
class PerformanceMonitor {
  constructor(pulsora) {
    this.pulsora = pulsora;
    this.metrics = [];
  }

  async trackEventWithMetrics(eventName, properties) {
    const startTime = performance.now();

    try {
      await this.pulsora.event(eventName, properties);
      const duration = performance.now() - startTime;

      this.metrics.push({
        event: eventName,
        duration,
        timestamp: Date.now(),
      });

      // Warn if slow
      if (duration > 100) {
        console.warn(
          `Slow analytics event: ${eventName} took ${duration.toFixed(2)}ms`,
        );
      }
    } catch (error) {
      console.error('Analytics error:', error);
    }
  }

  getStats() {
    const durations = this.metrics.map((m) => m.duration);
    return {
      count: durations.length,
      avg: durations.reduce((a, b) => a + b, 0) / durations.length,
      max: Math.max(...durations),
      min: Math.min(...durations),
    };
  }
}

// Usage
const monitor = new PerformanceMonitor(pulsora);
monitor.trackEventWithMetrics('purchase', { value: 99 });

// Check stats periodically
setInterval(() => {
  console.log('Analytics performance:', monitor.getStats());
}, 60000);

Network Request Monitoring#

// Monitor Pulsora network requests
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name.includes('pulsora.co')) {
      console.log('Pulsora request:', {
        url: entry.name,
        duration: entry.duration.toFixed(2) + 'ms',
        size: entry.transferSize + ' bytes',
        protocol: entry.nextHopProtocol,
      });
    }
  }
});

observer.observe({ entryTypes: ['resource'] });

Best Practices#

Do's#

  • ✅ Use async script tag for CDN integration
  • ✅ Load analytics after critical resources
  • ✅ Use code splitting for large apps
  • ✅ Enable tree shaking in production builds
  • ✅ Monitor performance metrics regularly
  • ✅ Use requestIdleCallback for non-critical tracking
  • ✅ Cache the Pulsora instance globally

Don'ts#

  • ❌ Don't initialize analytics in render loops
  • ❌ Don't track every small interaction
  • ❌ Don't block page load for analytics
  • ❌ Don't import entire package when you need specific exports
  • ❌ Don't initialize multiple instances unnecessarily
  • ❌ Don't forget to measure impact with real metrics

Troubleshooting Performance Issues#

High Memory Usage#

Symptom: Memory leak over time

Cause: Event listeners not cleaned up

Solution:

// Store reference to cleanup
const cleanup = [];

function setupAnalytics() {
  const handler = () => pulsora.pageview();
  window.addEventListener('popstate', handler);

  cleanup.push(() => {
    window.removeEventListener('popstate', handler);
  });
}

// Cleanup when needed
function teardown() {
  cleanup.forEach((fn) => fn());
}

Slow Event Tracking#

Symptom: Events take >100ms to send

Solutions:

  1. Use sendBeacon API:
pulsora.init({
  apiToken: 'pub_xxx',
  useBeacon: true, // Faster for fire-and-forget events
});
  1. Reduce event payload size:
// ❌ Bad - large payload
pulsora.event('purchase', {
  entire_cart: veryLargeCartObject, // Avoid large objects
});

// ✅ Good - small payload
pulsora.event('purchase', {
  item_count: cart.length,
  total_value: cart.total,
});

Next Steps#