I rebuilt a 12-screen onboarding flow last year. The original took 45 seconds to complete β not because there were 12 screens, but because of how it was built. After optimization it took 11 seconds. Same logic, same screens, dramatically different experience.
Screen flow performance isnβt mysterious. There are a handful of well-understood patterns that cause slowness, and a matching set of techniques that eliminate them. Let me walk through what Iβve learned.
Where Screen Flows Lose Time
Before optimizing, it helps to understand where time actually goes in a screen flow execution.
The data retrieval phase dominates. Every Get Records element before a screen is displayed causes a server round-trip. Three Get Records before the first screen means three sequential calls to the database before the user sees anything.
The Problem
A 12-screen onboarding flow took 45 seconds end-to-end. Profiling in Debug mode revealed 3 Get Records elements running sequentially before the first screen β querying Account, primary Contact, and a related custom config object independently. Each took 700-900ms. Users were staring at a spinner for 3+ seconds before seeing screen one.
The Solution
The config object query was replaced with a Formula resource (the value was derivable from the Accountβs Industry field). The Account and Contact queries were merged into one Get Records using a relationship query. First-screen load time dropped from 3.1 seconds to under 0.9 seconds with no logic changes.
Rule 1: Collapse Pre-Screen Get Records
The most impactful change I make in almost every optimization: reduce the number of Get Records elements that execute before the first screen.
Before: Three separate Get Records elements, each querying a different object.
After: One Get Records that queries the primary object with all needed fields, then use Formula resources and collection filters to derive the rest.
Ask yourself for each Get Records: is this value something I could compute from data I already have? Could it be a formula? Could it be merged with another query?
If two Get Records queries are independent of each other, Salesforce does not parallelize them β they run sequentially. The fewer, the faster.
Rule 2: Move Formulas in Front of Get Records
Formula resources evaluate in memory with zero latency. Before adding a Get Records, ask: can I derive this with a formula?
Examples of what formulas can replace:
- Calculating a due date from today + a constant
- Constructing a record name from other variables
- Determining which approval path to take based on the triggering recordβs fields
- Formatting a currency value for display
A formula that runs in 0ms beats a Get Records that runs in 400ms. Use them aggressively.
Rule 3: Reactive Screens β Zero Round-Trips for Dependent Fields
Reactive Screens is one of the most underused performance features in Screen Flows. When enabled, interactions between components on the same screen happen entirely client-side β no server call.
How it works
Normally, when a user changes a picklist that controls the visibility of another field, the flow sends a request to the server to re-evaluate conditions and re-render affected components. With reactive screens, this evaluation happens in the browser.
- Not all standard screen components support reactive behavior yet
- Custom LWC components need to be built with
lightning-flow-supportto participate - Formulas on reactive screens must use only screen-local variables (no cross-screen references)
For wizard-style flows where users make selections that affect visible fields on the same screen, reactive screens eliminate the most noticeable latency users experience.
Rule 4: Choice Sets vs. Dynamic Choice Sets
Static Choices
Static choices β hardcoded options, zero server cost. Best for options that rarely change.
Picklist Choice Sets
Choice sets from object picklist fields β fetched once, lightweight. Good for standard picklist values.
Dynamic Choice Sets
Dynamic choice sets from Get Records β runs a SOQL query every time the screen loads. Use only when options genuinely vary per record or user.
Dynamic choice sets are often used unnecessarily. If the options donβt change record-by-record (e.g., a list of all active Product Categories), consider whether a static choice set would serve equally well β it loads instantly.
Efficient Dynamic Choice Pattern
If you do need dynamic choices, pre-load the data with a single Get Records early in the flow and store it in a collection variable. Then reference that pre-loaded collection in your Dynamic Choice Set, rather than letting the choice set trigger its own query on each screen render.
// Flow structure for efficient dynamic choices:
// 1. [Get Records: All Active Products β Products_Collection]
// 2. [Screen 1 - uses Products_Collection for choice set]
// 3. [Screen 2 - reuses Products_Collection, no new query]Rule 5: Minimize Data Passed Between Screens
Every variable held in flow memory is serialized and passed to the server on each navigation between screens. Large collection variables β a list of 500 records, for example β add serialization overhead on every Next button click.
Best practice: only load what you need for the current and next screen. If a later step needs data that depends on user input from an earlier step, do that Get Records after the relevant screen, not before the first screen.
Rule 6: Lazy Loading with Conditional Paths
Donβt load data for a path the user might not take. If your flow has a branch where 80% of users go left and 20% go right, and the right branch needs a complex Get Records β put that Get Records inside the right-branch path, not before the Decision element.
Correct: Lazy Loading
[Screen: Which path?]
β User selects "Advanced Options"
[Get Records: Advanced config data] β Only loads when needed
β
[Screen: Advanced Options screen]Anti-Pattern: Eager Loading
[Get Records: Advanced config data] β Loads for ALL users even if they never reach it
[Screen: Which path?]This is especially wasteful for multi-step wizards where early screens collect the information needed to determine which subsequent data to load.
Rule 7: Assignment Instead of Get Records for Derived Values
A pattern I see frequently: a Get Records that queries a single record purely to read one field value that could have been passed in as an input variable.
If a screen flow is launched from a record page (via a Quick Action or Utility Bar), use the recordβs Id as an input variable and pass in the fields you need as additional input variables from the launching context. The Flow Orchestration and Quick Action framework support this.
This avoids the Get Records entirely β the data is already available at flow start.
Rule 8: DML Consolidation at the End
Every Create Records or Update Records element is a DML statement and a server round-trip. If your flow creates 3 records across 3 separate elements, thatβs 3 DML operations.
DML Consolidation Pattern
Better pattern: use Assignment elements to build up records into collection variables throughout the flow, then do a single Create Records or Update Records at the end with the full collection.
// Build up during flow:
[Assignment: Add Contact to New_Contacts_Collection]
[Assignment: Add Case to New_Cases_Collection]
// Single DML at completion:
[Create Records: New_Contacts_Collection]
[Create Records: New_Cases_Collection]Two DML operations instead of potentially many, and the user reaches the success screen faster.
Itβs tempting to start removing Get Records elements the moment a flow feels slow. But in practice, the bottleneck is often not where you expect. Run the flow in Debug mode first and expand each element to check its execution time. Iβve spent an hour refactoring Get Records only to discover the real culprit was a custom LWC component making its own Apex callout on load. Profile first, then fix the actual slowest element. Repeat until itβs fast.
Rule 9: Profile Your Flow in Debug Mode
The Flow Debug panel shows exactly how long each element took to execute. Before optimizing blindly, run the flow in debug mode and identify the actual bottleneck. Iβve been wrong about where time was being spent more than once.
Steps:
- Open the Flow in Builder
- Click Debug
- Fill in test values and run
- Expand each element in the debug output to see execution time
Sort by time spent. Fix the slowest element first. Repeat.
Rule 10: LWC Components in Screen Flows
If your Screen Flow embeds custom Lightning Web Components, those components are subject to their own performance considerations:
- Use
@wireadapters instead of@AuraEnabledimperative calls where possible - Avoid making Apex calls in
connectedCallbackif the data can be passed in as a Flow input variable - Implement
lightning-flow-supportfor reactive behavior - Keep component state minimal β large component state serializes across screen transitions
A poorly optimized LWC in a screen flow can add 1-2 seconds per screen independently of everything else in the flow.
The Optimization I Run Every Time
When I take over a slow screen flow, I do these five things in order:
- Run in debug mode and identify the top 3 slowest elements
- Move any Get Records that occur before the first screen β can any be removed or combined?
- Replace Get Records with formulas wherever the data can be derived
- Enable Reactive Screens and verify dependent fields work correctly
- Consolidate DML into collection operations at the end
In my experience, steps 2 and 3 alone account for 60-70% of the performance improvement in most flows.
Whatβs the slowest screen flow in your org right now, and what do you think the main culprit is? If youβve profiled it in Debug mode and found a surprise bottleneck, share it in the comments β those real-world examples are always the most useful.
Knowledge Check
How did this article make you feel?
Comments
Salesforce Tip