# How to Optimize Retool App Performance

- Tool: Retool
- Difficulty: Advanced
- Time required: 30-40 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## TL;DR

The most impactful Retool performance fixes in order: (1) reduce query payload below 1.6MB by limiting columns and adding pagination, (2) disable page-load execution on queries that users only need sometimes, (3) enable caching on slow aggregate queries, (4) split large apps into multipage apps, (5) debounce input-triggered queries. The Debug Panel Performance tab grades your app from A-F and identifies specific issues.

## Why Retool Apps Get Slow and How to Fix Each Cause

A slow Retool app is usually caused by one of five issues: (1) query payloads too large — fetching thousands of rows or wide result sets, (2) too many queries on page load — every query runs simultaneously when the page opens, (3) slow database queries — missing indexes, expensive joins, or unoptimized SQL, (4) too many components — apps with 50+ components have high render overhead, (5) no caching — identical queries running repeatedly instead of serving from cache.

The Debug Panel Performance tab in Retool grades each issue from A-F and shows specific recommendations. This tutorial walks through fixing each grade below B, from easiest to most impactful.

## Before you start

- A Retool app that loads slowly or has user-reported performance issues
- Access to the Retool Debug Panel (click the bug icon)
- Admin or Editor access to modify query settings and app configuration

## Step-by-step guide

### 1. Run a performance audit in the Debug Panel

Open your app in edit mode and click the bug icon (bottom-left) to open the Debug Panel. Switch to the Performance tab. Retool grades your app in several categories: Queries (payload size, count), Components (total count), and Network (response times). Review each grade — anything below B is worth fixing. Note the specific issues listed under each grade: 'Query X returns 50,000 rows' or 'App has 65 components — consider splitting into pages'. Start with the F and D grades first.

**Expected result:** Performance audit shows grades for each category with specific issues and recommendations.

### 2. Reduce query payload size below 1.6MB

The most common performance killer is fetching too much data. A query returning 10,000 rows with 20 columns generates megabytes of JSON that the browser must parse and render. Fix this by: (1) SELECT only the columns you display, (2) add a WHERE clause to filter down the result set, (3) implement LIMIT for all queries without pagination. Retool's recommended payload limit is 1.6MB per query — the Debug Panel shows each query's payload size.

```
-- ❌ Before: SELECT * returns all 40 columns
SELECT * FROM orders WHERE status = 'active';

-- ✅ After: SELECT only displayed columns
SELECT
  id, order_number, customer_name, status,
  total_amount, created_at
FROM orders
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 500; -- Cap at 500 rows until pagination is added
```

**Expected result:** Query payload drops below 1.6MB. Page load time decreases proportionally.

### 3. Disable page-load queries that aren't needed immediately

Every query with 'Run on page load' enabled fires simultaneously when the app opens. If your app has 10 queries all running on load, the page must wait for all 10 to complete. Audit each query: is it needed for the initial view, or only when the user interacts? For queries behind tabs, modals, or filter selections — disable 'Run on page load' and instead trigger them from an event handler or via 'Run when inputs change' (which only runs when their inputs have values).

**Expected result:** Page load only runs the minimum required queries. Tab/modal queries load on demand.

### 4. Enable server-side pagination for large tables

Client-side pagination (the default) fetches all rows then paginates in the browser. Server-side pagination fetches only the current page's rows. Enable server-side pagination by modifying your SQL query to use LIMIT and OFFSET tied to the table's pagination properties: {{ table1.pageSize }} (default 10) and {{ table1.pageIndex * table1.pageSize }}. Also add a count query for total pages. This keeps payloads consistently small regardless of table size.

```
-- Server-side paginated query
SELECT
  id, name, status, created_at
FROM orders
WHERE status = {{ statusFilter.value || 'active' }}
ORDER BY created_at DESC
LIMIT {{ table1.pageSize }}
OFFSET {{ table1.pageIndex * table1.pageSize }};

-- Companion count query for total pages
SELECT COUNT(*) AS total
FROM orders
WHERE status = {{ statusFilter.value || 'active' }};

-- In the Table Inspector:
-- Pagination type: Manual (server-side)
-- Total rows: {{ countQuery.data[0].total }}
```

**Expected result:** Table fetches only 10-50 rows per page instead of all records. Query payload stays under 100KB regardless of total data size.

### 5. Enable caching on slow aggregate queries

Aggregate queries (COUNT, SUM, AVG, GROUP BY) are often the slowest in your app because they scan entire tables. Enable caching in the query's Advanced tab. Set a TTL appropriate for how often the underlying data changes — a daily summary query might cache for 5 minutes, a static product catalog for 1 hour. After enabling caching, use await queryName.invalidateCache() in write operation success handlers to ensure stale data doesn't persist after modifications.

**Expected result:** Slow aggregate queries serve cached results in milliseconds. Database load decreases significantly.

### 6. Split large apps into multipage apps

Retool's Debug Panel gives a component count performance grade. Apps with 50+ components have high render overhead. The solution is converting a single large app into a multipage app: click Settings → Convert to multipage app. Create separate pages for distinct sections (Dashboard, Orders, Customers, Settings). Each page loads only its own queries and components — reducing the initial load and improving perceived performance significantly.

**Expected result:** App splits into pages. Each page loads faster with fewer components and queries per view.

### 7. Debounce input-triggered queries

If a query has 'Run when inputs change' enabled and is triggered by a text input (like a search box), it fires on every keystroke. A user typing 'customer name' fires the query 13 times. Add a debounce by using a JS Query as intermediary: instead of the query directly watching the text input, watch a Temporary State variable. A 'debounce' JS Query (not on page load) sets the state variable with a 300ms delay after input changes.

```
// In the text input's On change event handler:
// Trigger JS Query 'debounceSearch'

// JS Query: debounceSearch
// Triggered by search input's On change
let debounceTimer;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
  await searchQuery.trigger();
}, 300);

// searchQuery references searchInput.value in its SQL WHERE clause
```

**Expected result:** Search query fires once after the user stops typing (300ms pause) instead of on every keystroke.

## Complete code example

File: `SQL Query: optimizedPaginatedQuery`

```sql
-- Fully optimized paginated query with server-side filtering
-- Combines: specific column selection, server-side pagination,
-- dynamic filtering, and prepared statement parameters

SELECT
  o.id,
  o.order_number,
  c.name AS customer_name,
  o.status,
  o.total_amount,
  o.created_at
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE
  o.status = COALESCE({{ statusSelect.value || null }}, o.status)
  AND o.created_at >= COALESCE({{ dateRangePicker1.value[0] || null }}, '2000-01-01'::date)
  AND o.created_at <= COALESCE({{ dateRangePicker1.value[1] || null }}, NOW())
  AND (
    {{ !searchInput.value }}
    OR c.name ILIKE {{ '%' + (searchInput.value || '') + '%' }}
    OR o.order_number ILIKE {{ '%' + (searchInput.value || '') + '%' }}
  )
ORDER BY o.created_at DESC
LIMIT {{ table1.pageSize || 20 }}
OFFSET {{ (table1.pageIndex || 0) * (table1.pageSize || 20) }};
```

## Common mistakes

- **Using SELECT * in all queries without realizing it returns dozens of unnecessary columns** — undefined Fix: Audit each query's result schema in the Debug Panel. Identify columns that are never displayed and remove them from the SELECT list.
- **Enabling 'Run on page load' on 15 queries that all fire simultaneously on page open** — undefined Fix: Categorize each query: required for initial view (keep page load), or only needed on interaction (disable page load, trigger from event handler). Aim for 3-5 queries on initial page load.
- **Having search input directly trigger a SQL query on every keystroke via 'Run when inputs change'** — undefined Fix: Add a debounce using a setTimeout in a JS Query between the input and the database query. This prevents database queries on every keystroke and is critical for search functionality.

## Best practices

- Run a Debug Panel Performance audit before and after optimizations to measure actual improvement
- Target query payloads under 1.6MB each — this is the single biggest performance lever in most apps
- Use SELECT specific columns instead of SELECT * — it reduces payload, network transfer, and memory usage
- Implement server-side pagination for any table showing more than 200 rows
- Cache slow aggregate queries with TTLs matching data change frequency
- Keep total component count per page under 40 — consider multipage apps for larger applications

## Frequently asked questions

### What is the recommended maximum payload size for Retool queries?

Retool recommends keeping individual query payloads under 1.6MB (1,600,000 bytes). The Debug Panel Performance tab shows each query's payload size and flags any that exceed this threshold. Payloads above 5MB can cause the browser to become unresponsive during rendering.

### How many components can a Retool app have before performance degrades?

Retool's Debug Panel gives a performance grade for component count. Apps with under 30 components typically get A grades. 30-50 components is B-C range. Above 50 components, Retool recommends splitting into a multipage app. Component count is the cumulative total including hidden components — hidden components still consume memory and render time.

### Does the number of queries in a Retool app affect performance even if they don't run on page load?

Queries that don't run on page load (disabled from 'Run on page load') do not affect initial load time. They only consume resources when triggered. However, having many queries in your app's codebase can slow down the Retool editor itself — consider consolidating queries with similar purposes.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-optimize-retool-app-performance
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-optimize-retool-app-performance
