All posts

Remix Performance Monitoring Best Practices

Monitor Remix application performance with loader timing, action profiling, streaming metrics, and server-side rendering optimization.

Remix Performance Monitoring Best Practices

Remix's server-first architecture shifts performance concerns to loaders and actions. Here's how to monitor what matters.

Monitor Loader Performance

Loaders are where most time is spent. Instrument them:

import { json, type LoaderFunctionArgs } from '@remix-run/node';

function withTiming<T>(name: string, fn: () => Promise<T>): Promise<T> {
  const start = performance.now();
  return fn().finally(() => {
    const duration = performance.now() - start;
    console.log(JSON.stringify({
      type: 'loader_timing',
      name,
      duration_ms: Math.round(duration),
    }));
  });
}

export async function loader({ params }: LoaderFunctionArgs) {
  const [user, orders] = await Promise.all([
    withTiming('getUser', () => getUser(params.id)),
    withTiming('getOrders', () => getOrders(params.id)),
  ]);
  return json({ user, orders });
}

Track Action Performance

export async function action({ request }: ActionFunctionArgs) {
  const start = performance.now();
  const formData = await request.formData();

  try {
    const result = await processOrder(formData);
    return json(result);
  } finally {
    console.log(JSON.stringify({
      type: 'action_timing',
      route: '/orders',
      method: request.method,
      duration_ms: Math.round(performance.now() - start),
    }));
  }
}

Server Timing Headers

Remix supports Server-Timing headers for browser DevTools visibility:

export async function loader({ request }: LoaderFunctionArgs) {
  const start = performance.now();
  const data = await fetchData();
  const duration = performance.now() - start;

  return json(data, {
    headers: {
      'Server-Timing': `db;dur=${Math.round(duration)}`,
    },
  });
}

Key Metrics

  • Loader execution time — p95 per route
  • Action execution time — mutations should complete within 200ms
  • Time to First Byte — measures SSR speed
  • Cache hit ratio — for loaders using HTTP caching
  • Waterfall depth — nested route loaders run in parallel; verify they do

Streaming Performance

If using defer(), monitor both the initial response and streamed data:

export async function loader() {
  return defer({
    critical: await getCriticalData(),  // Blocks initial response
    deferred: getSlowData(),            // Streamed later
  });
}

Track how long deferred promises take to resolve. Bugsly captures errors from both the initial render and deferred data loading, ensuring nothing falls through the cracks in your Remix application.

Try Bugsly Free

AI-powered error tracking that explains your bugs. Set up in 2 minutes, free forever for small projects.

Get Started Free