Frontend monitoring provides critical insights into how your application performs in real-world conditions. Unlike backend monitoring, frontend monitoring captures the actual user experience, including device performance, network conditions, and browser compatibility. This comprehensive guide covers everything you need to implement production-grade frontend monitoring.
Why Frontend Monitoring Matters
Frontend issues directly impact user experience and business metrics. Studies show that:
- 53% of mobile users abandon sites that take longer than 3 seconds to load
- A 100ms increase in load time decreases conversion rates by 1%
- Frontend errors account for 76% of all user-reported issues
Frontend monitoring helps you:
- Detect performance regressions before users notice
- Identify browser and device-specific issues
- Track real-user performance metrics
- Debug JavaScript errors in production
- Monitor API performance from the client perspective
Core Monitoring Metrics
Track these essential metrics to understand frontend health.
Core Web Vitals
Google's Core Web Vitals measure real-world user experience:
// Measure Largest Contentful Paint (LCP)
import { getLCP } from 'web-vitals';
getLCP((metric) => {
console.log('LCP:', metric.value);
// Send to monitoring service
trackMetric('lcp', metric.value);
});
// Measure First Input Delay (FID)
import { getFID } from 'web-vitals';
getFID((metric) => {
console.log('FID:', metric.value);
trackMetric('fid', metric.value);
});
// Measure Cumulative Layout Shift (CLS)
import { getCLS } from 'web-vitals';
getCLS((metric) => {
console.log('CLS:', metric.value);
trackMetric('cls', metric.value);
});Custom Performance Metrics
Track domain-specific metrics:
// Time to Interactive
function measureTTI() {
const startTime = performance.now();
// Wait until page is interactive
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
const tti = performance.now() - startTime;
trackMetric('tti', tti);
}
}
});
observer.observe({ entryTypes: ['paint'] });
}
// Component Render Time
function measureComponentRender(componentName: string) {
const start = performance.now();
return () => {
const duration = performance.now() - start;
trackMetric('component_render_time', duration, {
component: componentName
});
};
}
// API Response Time
async function trackAPICall(url: string) {
const start = performance.now();
try {
const response = await fetch(url);
const duration = performance.now() - start;
trackMetric('api_response_time', duration, {
url,
status: response.status
});
return response;
} catch (error) {
const duration = performance.now() - start;
trackMetric('api_error', duration, {
url,
error: error.message
});
throw error;
}
}Error Monitoring
Capture and track JavaScript errors effectively.
Global Error Handler
// error-handler.ts
interface ErrorContext {
userAgent: string;
url: string;
timestamp: number;
userId?: string;
sessionId: string;
}
class ErrorHandler {
private sessionId: string;
constructor() {
this.sessionId = crypto.randomUUID();
this.setupGlobalHandlers();
}
private setupGlobalHandlers() {
// Catch unhandled errors
window.addEventListener('error', (event) => {
this.captureError(event.error, {
type: 'unhandled_error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
// Catch unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.captureError(event.reason, {
type: 'unhandled_rejection'
});
});
// Catch resource loading errors
window.addEventListener('error', (event) => {
if (event.target !== window) {
this.captureError(new Error('Resource load failed'), {
type: 'resource_error',
tagName: (event.target as any).tagName,
src: (event.target as any).src
});
}
}, true);
}
captureError(error: Error, additionalContext?: any) {
const context: ErrorContext = {
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: Date.now(),
sessionId: this.sessionId
};
// Send to monitoring service
this.sendToMonitoring({
error: {
name: error.name,
message: error.message,
stack: error.stack
},
context,
additionalContext
});
}
private sendToMonitoring(data: any) {
// Send to your monitoring service
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).catch(console.error);
}
}
export const errorHandler = new ErrorHandler();React Error Boundary
// ErrorBoundary.tsx
import React from 'react';
interface Props {
children: React.ReactNode;
fallback?: React.ComponentType<{ error: Error }>;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Capture error with React component stack
errorHandler.captureError(error, {
componentStack: errorInfo.componentStack,
errorBoundary: true
});
}
render() {
if (this.state.hasError) {
const FallbackComponent = this.props.fallback || DefaultErrorFallback;
return <FallbackComponent error={this.state.error!} />;
}
return this.props.children;
}
}
function DefaultErrorFallback({ error }: { error: Error }) {
return (
<div style={{ padding: '20px', backgroundColor: '#fee' }}>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}Performance Monitoring
Monitor and analyze application performance.
Performance Observer API
// performance-monitor.ts
class PerformanceMonitor {
constructor() {
this.setupObservers();
}
private setupObservers() {
// Monitor long tasks
if ('PerformanceObserver' in window) {
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
trackMetric('long_task', entry.duration, {
name: entry.name,
startTime: entry.startTime
});
}
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
// Monitor resource timing
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as PerformanceResourceTiming[]) {
trackMetric('resource_timing', entry.duration, {
name: entry.name,
type: entry.initiatorType,
size: entry.transferSize
});
}
});
resourceObserver.observe({ entryTypes: ['resource'] });
}
}
measureRender(componentName: string) {
const start = performance.now();
return () => {
const duration = performance.now() - start;
trackMetric('render_time', duration, {
component: componentName
});
};
}
}
export const performanceMonitor = new PerformanceMonitor();User Timing API
// Measure custom operations
function measureOperation(name: string, operation: () => void) {
performance.mark(`${name}-start`);
operation();
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
const measure = performance.getEntriesByName(name)[0];
trackMetric('custom_operation', measure.duration, {
name
});
}
// Usage
measureOperation('data_processing', () => {
// Heavy computation
processLargeDataset(data);
});Real User Monitoring (RUM)
Collect real user performance data from production.
RUM Implementation
// rum-collector.ts
interface RUMData {
sessionId: string;
userId?: string;
pageLoad: {
navigationTiming: PerformanceNavigationTiming;
resourceTimings: PerformanceResourceTiming[];
};
webVitals: {
lcp?: number;
fid?: number;
cls?: number;
};
customMetrics: Array<{
name: string;
value: number;
timestamp: number;
}>;
}
class RUMCollector {
private sessionId: string;
private data: Partial<RUMData> = {
sessionId: crypto.randomUUID(),
customMetrics: []
};
constructor() {
this.sessionId = crypto.randomUUID();
this.collectNavigationTiming();
this.collectWebVitals();
}
private collectNavigationTiming() {
const [navigation] = performance.getEntriesByType(
'navigation'
) as PerformanceNavigationTiming[];
this.data.pageLoad = {
navigationTiming: navigation,
resourceTimings: performance.getEntriesByType(
'resource'
) as PerformanceResourceTiming[]
};
}
private collectWebVitals() {
import('web-vitals').then(({ getLCP, getFID, getCLS }) => {
getLCP((metric) => {
this.data.webVitals = { ...this.data.webVitals, lcp: metric.value };
});
getFID((metric) => {
this.data.webVitals = { ...this.data.webVitals, fid: metric.value };
});
getCLS((metric) => {
this.data.webVitals = { ...this.data.webVitals, cls: metric.value };
});
});
}
addCustomMetric(name: string, value: number) {
this.data.customMetrics?.push({
name,
value,
timestamp: Date.now()
});
}
async send() {
await fetch('/api/rum', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.data)
});
}
}
export const rumCollector = new RUMCollector();Session Recording
Record and replay user sessions for debugging.
Session Replay Implementation
// session-recorder.ts
class SessionRecorder {
private events: Array<{
type: string;
timestamp: number;
data: any;
}> = [];
constructor() {
if (this.shouldRecord()) {
this.startRecording();
}
}
private shouldRecord() {
// Sample 10% of sessions
return Math.random() < 0.1;
}
private startRecording() {
// Record DOM mutations
const mutationObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
this.events.push({
type: 'mutation',
timestamp: Date.now(),
data: mutation
});
});
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// Record clicks
document.addEventListener('click', (event) => {
this.events.push({
type: 'click',
timestamp: Date.now(),
data: {
target: (event.target as HTMLElement).tagName,
x: event.clientX,
y: event.clientY
}
});
});
// Record navigation
let lastUrl = window.location.href;
setInterval(() => {
if (window.location.href !== lastUrl) {
lastUrl = window.location.href;
this.events.push({
type: 'navigation',
timestamp: Date.now(),
data: { url: lastUrl }
});
}
}, 1000);
}
async saveSession() {
await fetch('/api/session-replay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: crypto.randomUUID(),
events: this.events
})
});
}
}
export const sessionRecorder = new SessionRecorder();Analytics Integration
Combine monitoring with user analytics.
Custom Events
// analytics.ts
interface AnalyticsEvent {
name: string;
properties?: Record<string, any>;
userId?: string;
timestamp: number;
}
class Analytics {
private queue: AnalyticsEvent[] = [];
track(name: string, properties?: Record<string, any>) {
this.queue.push({
name,
properties,
timestamp: Date.now()
});
// Flush queue every 30 seconds
if (this.queue.length >= 10) {
this.flush();
}
}
async flush() {
if (this.queue.length === 0) return;
await fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events: this.queue })
});
this.queue = [];
}
}
export const analytics = new Analytics();
// Usage
analytics.track('button_clicked', {
button_id: 'submit_form',
page: '/checkout'
});Alerting
Set up intelligent alerts for critical issues.
Alert Rules
// alerting.ts
interface AlertRule {
name: string;
condition: (metrics: any) => boolean;
severity: 'low' | 'medium' | 'high';
notification: string[];
}
class AlertManager {
private rules: AlertRule[] = [
{
name: 'High Error Rate',
condition: (metrics) => metrics.errorRate > 0.05,
severity: 'high',
notification: ['slack', 'email']
},
{
name: 'Slow Page Load',
condition: (metrics) => metrics.avgPageLoadTime > 3000,
severity: 'medium',
notification: ['slack']
},
{
name: 'Poor Core Web Vitals',
condition: (metrics) =>
metrics.poorLCP > 0.1 ||
metrics.poorFID > 0.1 ||
metrics.poorCLS > 0.25,
severity: 'medium',
notification: ['slack']
}
];
checkRules(metrics: any) {
this.rules.forEach(rule => {
if (rule.condition(metrics)) {
this.sendAlert(rule, metrics);
}
});
}
private sendAlert(rule: AlertRule, metrics: any) {
// Send notification based on severity
console.error(`ALERT [${rule.severity}]: ${rule.name}`, metrics);
}
}
export const alertManager = new AlertManager();Best Practices
Follow these practices for effective monitoring:
- 1Sample Rate: Implement smart sampling to reduce costs while maintaining statistical significance
- 2Privacy: Anonymize PII and comply with GDPR
- 3Performance: Ensure monitoring code doesn't degrade performance
- 4Context: Capture rich context with every error
- 5Dashboards: Create visual dashboards for quick insights
- 6Alert Fatigue: Configure alerts carefully to avoid alert fatigue
- 7Testing: Test monitoring code in staging before production
Conclusion
Frontend monitoring is essential for delivering excellent user experiences. By implementing comprehensive monitoring covering performance, errors, and user behavior, you can proactively identify and fix issues before they impact your users.
Start with core metrics like Web Vitals, then expand to cover your specific use cases. Remember: what you can measure, you can improve.