The dashboard opens, and dozens of queries fire before the first table renders. Three of them fetch the same customer list. Two keep refreshing a section nobody has scrolled to.
None of those queries was a mistake when it was added. Each one solved a real problem on the day it shipped. Together they make a page that gets slower every month. People start refreshing it, keeping their own copies of the data, and working around it.
This is the third pillar of internal tools literacy, after systems logic and integration awareness. Good workflows and good connections still depend on an app that can carry the load.
What performance architecture means
Performance architecture is the set of structural decisions that shape how a tool behaves as users, data, and workflows grow. Three of them do most of the work: how the app is split into pages, when and how much each query fetches, and what gets cached.
Made deliberately, they let the tool grow with the team. Left to accumulate, they make it heavier with each release, in ways that are hard to trace to one cause.
Why internal tools slow down as they grow
The tool usually worked well at launch, because it matched the team at that moment. Then the team grew, the tool took on more workflows, and the structure stayed the same.
The common shape is one page that keeps collecting responsibilities. Every new workflow adds queries to it. Every new integration adds data movement. Eventually the page runs everything on load, including work unrelated to what the user came to do.
The slowdown is gradual, which is why it goes unnoticed. First the page feels a little slower. Then people wait. Then they stop trusting what they see, and a performance problem has become an adoption problem.
Unscoped integrations make it worse. A query that returns a whole table, when the screen shows one customer's orders, is fast while the table is small. It gets slower every week the table grows.
Step 1: find what the page does on load
Start with the slowest tool your team uses. Open it, note where you wait, and ask one question: what is this page doing when it loads?
In Retool, the debug panel is the place to look. Its performance tab shows query count, load time, and app size, so you can see where the load comes from before changing anything. Someone technical can usually read the query list and spot the redundant ones in a single sitting.
Write down what you find, page by page. The template at the end of this piece gives you a format for it.
Step 2: split the page by workflow
Many tools start as one dashboard that shows everything. Every query on that page runs on every load, whatever the user came to do.
Splitting it into focused pages changes that. Each page loads only the data and logic its workflow needs. Someone checking shipment status loads shipment data. A finance user building a report loads financial data, and never the inventory logic.
The signal to split: the page runs many queries on load, or it serves roles that do different jobs. Workflows that share neither data nor users usually belong on separate pages. Retool supports multipage apps, so the split does not mean a second app to maintain.
Step 3: scope every query
The second source of load is queries that run more often than they need to. They fire on state changes, clicks, or input events, even when the data underneath has not changed.
Two changes fix most of it. Run a query only when the data it returns could have changed. Fetch only the rows and columns the current screen shows.
-- Before: the whole table on every load, filtered in the app
-- select * from orders;
-- After: only the columns the table shows, for the selected customer
select id, status, total, updated_at
from orders
where customer_id = {{ customerSelect.value }}
order by updated_at desc
limit 100;The second version moves the filtering into the database, which is built for it. The app receives what the screen displays, and nothing else.
Heavy data reshaping inside the app is the related trap. If a transformer reshapes thousands of rows on every load, move that work into the query, or into a view in the database.
Step 4: cache what rarely changes
Some data is requested constantly and changes rarely: reference data, configuration settings, lookup tables. Querying the source every time adds load on both sides.
Cache it after the first request, with an expiry that matches how often it really changes. In Retool, a query's advanced settings let you cache its results for a set duration. A list of country codes can sit in the cache for a day. An order status cannot.
Query optimization and caching solve different problems. Optimization decides when a query runs and how much it returns. Caching decides whether it needs to run at all. Most slow pages need both.
Start with the change that removes the most waiting
Pick one pattern and fix it first. A page with many queries on load usually wants splitting. Data fetched repeatedly across workflows usually wants caching. Queries that fire on every interaction want scoping.
These decisions cost least in the first versions of a tool, while pages, triggers, and caching are still being set. They can be added later, and often are, as part of a fix. After the fix, keep them as design rules for every new workflow and integration.
Retool gives you the instruments for this: the debug panel, multipage apps, and caching on each query. The structural choices belong to whoever designs the app, and they hold on any platform.
The template
Copy this into the app's repository or wiki, and fill it in for each slow page.
page: orders_dashboard
roles_using_it: [support agent, finance analyst]
on_load:
query_count: 0 # from the debug panel's performance tab
load_time_seconds: 0
queries:
- name: getOrders
runs_on: page load # page load | input change | manual
returns: all rows # all rows | scoped to the selection
needed_on_this_page: true
duplicate_of: null # another query fetching the same data
data_changes: often # often | daily | rarely
action: scope # keep | scope | cache | move to another page
split_candidates: [] # workflows on this page that share no data or users
cache_candidates: [] # reference data, configuration, lookup tables
first_fix: "" # the one change that removes the most waitingWhat changes once it is in place
The first change shows in how people use the tool. They stop refreshing, stop keeping their own copies, and start relying on what the page shows.
The second change is structural. New workflows land on their own pages without slowing the others. New integrations connect without dragging existing screens down. New users join without degrading the tool for everyone else, and the team extends the tool instead of replacing it.
The rest of what a tool needs as usage grows is in Cool demo. Now what?
Our view
We recommend auditing the slowest page before touching anything else, because its query list usually points straight at the cause. The fix that follows is the cheapest one available. Then make page structure, query scope, and caching the rules for every new workflow. A tool built that way can grow without carrying the weight of its earlier versions.
