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 (
asyncattribute). - SPA auto-tracking runs after
DOMContentLoadedso it never blocks rendering. navigator.sendBeaconis 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
fetchto 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
- Visit webpagetest.org
- Test your site before adding Pulsora
- Add Pulsora with optimization
- 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
requestIdleCallbackfor 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:
- Use sendBeacon API:
pulsora.init({
apiToken: 'pub_xxx',
useBeacon: true, // Faster for fire-and-forget events
});
- 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
- Debugging Guide - Troubleshoot performance issues
- API Reference - Configuration options for performance
- Examples - Real-world optimized implementations