Skip to main content
Remix is designed for performance from the ground up, built on web standards and runtime-agnostic code. Follow these guidelines to get the most out of your Remix applications.

Response Compression

Compress responses to reduce bandwidth:
import { createRouter } from 'remix/fetch-router'
import { compression } from 'remix/compression-middleware'

let router = createRouter({
  middleware: [
    compression({
      threshold: 1024, // Only compress responses > 1KB
      level: 6, // Compression level (1-9)
    }),
  ],
})
Or compress specific responses:
import { compressResponse } from 'remix/response/compress'

router.get(routes.data, async ({ request }) => {
  let data = await fetchLargeDataset()
  let response = Response.json(data)
  
  return compressResponse(response, request)
})

Caching Strategies

HTTP Caching

Use Cache-Control headers for browser caching:
import { CacheControl } from 'remix/headers'

router.get(routes.assets, async ({ params }) => {
  let file = await readFile(params.filename)
  
  return new Response(file, {
    headers: {
      'Content-Type': 'image/jpeg',
      'Cache-Control': CacheControl.stringify({
        public: true,
        maxAge: 31536000, // 1 year
        immutable: true,
      }),
    },
  })
})

ETag Support

Use ETags for conditional requests:
import { createFileResponse } from 'remix/response/file'

router.get(routes.file, async ({ request, params }) => {
  let file = await getFile(params.id)
  
  // createFileResponse automatically handles ETags and If-None-Match
  return createFileResponse(file, request)
})

Application-Level Caching

Cache expensive computations:
let cache = new Map<string, { value: any; expires: number }>()

function cached<T>(key: string, fn: () => Promise<T>, ttl = 60000): Promise<T> {
  let cached = cache.get(key)
  
  if (cached && cached.expires > Date.now()) {
    return Promise.resolve(cached.value)
  }

  return fn().then((value) => {
    cache.set(key, { value, expires: Date.now() + ttl })
    return value
  })
}

// Usage
router.get(routes.stats, async () => {
  let stats = await cached('stats', async () => {
    return await computeExpensiveStats()
  }, 5 * 60 * 1000) // Cache for 5 minutes

  return Response.json(stats)
})

Database Query Optimization

Select Only Needed Columns

// Bad: Select all columns
let users = await db.query(users).all()

// Good: Select only needed columns
let users = await db.query(users)
  .select({
    id: users.id,
    name: users.name,
    email: users.email,
  })
  .all()

Use Indexes

Add indexes for frequently queried columns:
import { createIndex } from 'remix/data-table/migrations'

let migration = {
  up(db) {
    createIndex(db, 'users', 'email', { unique: true })
    createIndex(db, 'orders', 'user_id')
    createIndex(db, 'orders', 'created_at')
  },
}

Batch Queries

Fetch related data in batches:
// Bad: N+1 query problem
let users = await db.query(users).all()
for (let user of users) {
  user.orders = await db.query(orders).where({ userId: user.id }).all()
}

// Good: Use relations for eager loading
let users = await db.query(users)
  .with({ orders: userOrders })
  .all()

Limit Query Results

// Always paginate large result sets
let page = parseInt(url.searchParams.get('page') || '1')
let perPage = 20

let users = await db.query(users)
  .orderBy('created_at', 'desc')
  .limit(perPage)
  .offset((page - 1) * perPage)
  .all()

Streaming Responses

Stream large responses instead of buffering:
router.get(routes.export, () => {
  let stream = new ReadableStream({
    async start(controller) {
      // Stream data in chunks
      for await (let chunk of generateLargeDataset()) {
        controller.enqueue(encoder.encode(JSON.stringify(chunk) + '\n'))
      }
      controller.close()
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'application/x-ndjson',
    },
  })
})

Static File Serving

Serve static files efficiently:
import { staticFiles } from 'remix/static-middleware'

let router = createRouter({
  middleware: [
    staticFiles('./public', {
      maxAge: 31536000, // 1 year for immutable assets
      immutable: true,
      // Only serve specific file types
      filter: (pathname) => {
        return /\.(js|css|png|jpg|svg|woff2)$/.test(pathname)
      },
    }),
  ],
})

Component Performance

Minimize Re-renders

// Bad: Creates new objects on every render
function App(handle: Handle) {
  return () => (
    <div css={{ color: 'red', padding: '10px' }}>
      Content
    </div>
  )
}

// Good: Move static styles to setup
function App(handle: Handle) {
  let styles = { color: 'red', padding: '10px' }
  
  return () => <div css={styles}>Content</div>
}

Use Dynamic Styles Only When Needed

// Bad: Use css prop for dynamic values
function ProgressBar(handle: Handle) {
  let progress = 0
  
  return () => (
    <div css={{ width: `${progress}%` }}>  {/* Creates new CSS rule on every update */}
      {progress}%
    </div>
  )
}

// Good: Use style prop for dynamic values
function ProgressBar(handle: Handle) {
  let progress = 0
  
  return () => (
    <div
      css={{ backgroundColor: 'blue' }}  {/* Static */}
      style={{ width: `${progress}%` }}  {/* Dynamic */}
    >
      {progress}%
    </div>
  )
}

Batch Updates

// Bad: Multiple updates
function App(handle: Handle) {
  let count = 0
  let doubled = 0
  
  return () => (
    <button on={{
      click() {
        count++
        handle.update()  // First render
        doubled = count * 2
        handle.update()  // Second render
      },
    }}>
      {count} / {doubled}
    </button>
  )
}

// Good: Single update
function App(handle: Handle) {
  let count = 0
  let doubled = 0
  
  return () => (
    <button on={{
      click() {
        count++
        doubled = count * 2
        handle.update()  // Single render
      },
    }}>
      {count} / {doubled}
    </button>
  )
}

Monitoring Performance

Request Timing

import { logger } from 'remix/logger-middleware'

let router = createRouter({
  middleware: [
    logger('%date %method %path %status %response-time ms'),
  ],
})

Custom Metrics

let requestDurations: number[] = []

function performanceMiddleware(): Middleware {
  return async (context, next) => {
    let start = performance.now()
    let response = await next()
    let duration = performance.now() - start
    
    requestDurations.push(duration)
    
    // Calculate p95
    if (requestDurations.length > 100) {
      let sorted = [...requestDurations].sort((a, b) => a - b)
      let p95 = sorted[Math.floor(sorted.length * 0.95)]
      console.log(`P95 response time: ${p95}ms`)
      requestDurations = []
    }

    return response
  }
}

Runtime-Specific Optimizations

Node.js

  • Use clustering to utilize all CPU cores
  • Enable HTTP/2 for multiplexing
  • Use --max-old-space-size for memory-intensive apps

Bun

  • Already fast by default
  • Use built-in SQLite for embedded databases
  • Leverage native APIs for maximum performance

Deno

  • Use KV for edge caching
  • Leverage Deno Deploy’s global network
  • Cache dependencies with DENO_DIR

Cloudflare Workers

  • Keep workers under 1MB
  • Use KV for persistent data
  • Leverage Durable Objects for stateful logic
  • Enable edge caching

Best Practices

  • Measure before optimizing
  • Compress responses
  • Implement caching at multiple levels
  • Optimize database queries
  • Stream large responses
  • Minimize component re-renders
  • Use appropriate status codes for caching
  • Monitor performance metrics

Compression

Response compression middleware

Data Table

Optimize database queries

Build docs developers (and LLMs) love