Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Optimize Retool App Performance

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.

What you'll learn

  • Identify performance bottlenecks using the Debug Panel Performance tab and query timing
  • Reduce query payload size below 1.6MB by selecting specific columns and adding server-side pagination
  • Disable page-load queries that are not needed until user interaction
  • Split large single-page apps into multipage apps to reduce initial component count
  • Cache slow aggregate queries with appropriate TTLs to reduce database load
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced8 min read30-40 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
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.

Quick facts about this guide
FactValue
ToolRetool
DifficultyAdvanced
Time required30-40 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

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.

Prerequisites

  • 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.

typescript
1-- Before: SELECT * returns all 40 columns
2SELECT * FROM orders WHERE status = 'active';
3
4-- After: SELECT only displayed columns
5SELECT
6 id, order_number, customer_name, status,
7 total_amount, created_at
8FROM orders
9WHERE status = 'active'
10ORDER BY created_at DESC
11LIMIT 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.

typescript
1-- Server-side paginated query
2SELECT
3 id, name, status, created_at
4FROM orders
5WHERE status = {{ statusFilter.value || 'active' }}
6ORDER BY created_at DESC
7LIMIT {{ table1.pageSize }}
8OFFSET {{ table1.pageIndex * table1.pageSize }};
9
10-- Companion count query for total pages
11SELECT COUNT(*) AS total
12FROM orders
13WHERE status = {{ statusFilter.value || 'active' }};
14
15-- In the Table Inspector:
16-- Pagination type: Manual (server-side)
17-- 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.

typescript
1// In the text input's On change event handler:
2// Trigger JS Query 'debounceSearch'
3
4// JS Query: debounceSearch
5// Triggered by search input's On change
6let debounceTimer;
7clearTimeout(debounceTimer);
8debounceTimer = setTimeout(async () => {
9 await searchQuery.trigger();
10}, 300);
11
12// 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 working example

SQL Query: optimizedPaginatedQuery
1-- Fully optimized paginated query with server-side filtering
2-- Combines: specific column selection, server-side pagination,
3-- dynamic filtering, and prepared statement parameters
4
5SELECT
6 o.id,
7 o.order_number,
8 c.name AS customer_name,
9 o.status,
10 o.total_amount,
11 o.created_at
12FROM orders o
13JOIN customers c ON o.customer_id = c.id
14WHERE
15 o.status = COALESCE({{ statusSelect.value || null }}, o.status)
16 AND o.created_at >= COALESCE({{ dateRangePicker1.value[0] || null }}, '2000-01-01'::date)
17 AND o.created_at <= COALESCE({{ dateRangePicker1.value[1] || null }}, NOW())
18 AND (
19 {{ !searchInput.value }}
20 OR c.name ILIKE {{ '%' + (searchInput.value || '') + '%' }}
21 OR o.order_number ILIKE {{ '%' + (searchInput.value || '') + '%' }}
22 )
23ORDER BY o.created_at DESC
24LIMIT {{ table1.pageSize || 20 }}
25OFFSET {{ (table1.pageIndex || 0) * (table1.pageSize || 20) }};

Common mistakes

Why it's a problem: Using SELECT * in all queries without realizing it returns dozens of unnecessary columns

How to avoid: Audit each query's result schema in the Debug Panel. Identify columns that are never displayed and remove them from the SELECT list.

Why it's a problem: Enabling 'Run on page load' on 15 queries that all fire simultaneously on page open

How to avoid: 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.

Why it's a problem: Having search input directly trigger a SQL query on every keystroke via 'Run when inputs change'

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

My Retool app is loading slowly. The Debug Panel shows: (1) query 'getAllOrders' returns 500KB and fetches 10,000 rows with SELECT *, (2) 15 queries run on page load, (3) app has 70 components. For each problem, give me the specific fix: the optimized SQL for getAllOrders with server-side pagination ({{ table1.pageSize }}, {{ table1.pageIndex }}), which queries I should disable on page load, and how to split into a multipage app.

Retool Prompt

Optimize a Retool app with: (1) SQL query using SELECT specific_columns with LIMIT {{ table1.pageSize }} and OFFSET {{ table1.pageIndex * table1.pageSize }}, (2) query caching in Advanced tab with TTL 300 for aggregate queries, (3) debounce pattern for search input using setTimeout in JS Query, (4) multipage app conversion with global variables for cross-page state. Show the Debug Panel Performance grade targets for each fix.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.