How to Optimize Performance for Large Lists in Dear ImGui Using ImGuiListClipper
Use ImGuiListClipper to render only the visible subset of large datasets by iterating from DisplayStart to DisplayEnd, eliminating CPU overhead from processing millions of off-screen elements.
When displaying massive datasets in Dear ImGui (ocornut/imgui), iterating over every item to create UI elements—even those scrolled out of view—can destroy frame rates. The ImGuiListClipper class solves this by clipping the iteration itself, allowing you to process only the rows currently visible in the window. This guide covers the internal architecture, implementation patterns, and optimization strategies derived directly from the Dear ImGui source code.
Why Large Lists Kill Performance Without Clipping
Rendering a list with thousands or millions of items using a naive for loop creates a massive CPU bottleneck. Even though ImGui only draws visible pixels, it still executes widget logic—calculating layouts, handling input, and building draw commands—for every single item.
ImGuiListClipper eliminates this waste by calculating which index range is actually visible based on scroll position and item height. According to the implementation in imgui.cpp, the clipper works with any widget type (tables, trees, custom rows) and discovers item heights on-the-fly when not provided explicitly.
How ImGuiListClipper Works Internally
The clipper operates through a five-phase lifecycle defined in imgui.cpp and imgui_internal.h:
Step 1: Construction and Initialization
The clipper stores the total item count and optional fixed item height during setup. In imgui.cpp lines 3327–3332, ImGuiListClipper::Begin initializes ItemsCount and prepares the internal state:
void ImGuiListClipper::Begin(int items_count, float items_height);
If you don't know the total count upfront (e.g., streaming data), you can pass INT_MAX and later define ranges using IncludeItemsByIndex.
Step 2: Height Measurement
When items_height is 0.0f, the clipper enters a measurement phase. During the first call to Step() (lines 4439–4450), it submits the first item to let ImGui calculate the actual height from cursor movement. This automatic discovery removes the need for manual height calculations.
Step 3: Visible Range Calculation
Once height is known, ImGuiListClipper_StepInternal (around lines 82–92) computes DisplayStart and DisplayEnd by dividing the window's scroll position by the item height. This determines exactly which indices fall within the viewport.
Step 4: Dynamic Range Inclusion
For virtualized data sources where the total count isn't known at initialization, call IncludeItemsByIndex(begin, end) (lines 89–96) to append visible ranges dynamically. This allows the clipper to adjust its internal bookkeeping without restarting.
Step 5: Cursor Positioning
When the loop completes, the destructor calls End() (lines 65–87), which seeks the cursor to the final Y offset of the list. This ensures subsequent widgets position correctly, maintaining the illusion that all items exist without having processed them.
Using ImGuiListClipper Flags
The clipper supports behavioral flags defined in imgui.h. The most important is:
ImGuiListClipperFlags_NoSetTableRowCounters— When clipping inside tables, this prevents the clipper from modifying internal row counters. Use this when your item height doesn't match the table's row height (see comment at lines 18–19 inimgui.cpp).
Set flags via the public Flags member before calling Step():
ImGuiListClipper clipper;
clipper.Begin(item_count, row_height);
clipper.Flags |= ImGuiListClipperFlags_NoSetTableRowCounters;
Choosing the Right Approach for Your Data
| Data Characteristic | Implementation Strategy |
|---|---|
| Uniform height | Pass fixed height to Begin() and loop through DisplayStart to DisplayEnd |
| Variable height | Pass 0.0f as height; clipper measures first visible item automatically |
| Unknown total | Begin with INT_MAX, use IncludeItemsByIndex() as data arrives |
| Table rows | Use NoSetTableRowCounters flag to prevent counter interference |
Performance Characteristics
The clipper provides three key optimizations:
- CPU reduction — Only visible items (typically < 5% of total) execute widget code
- Cache efficiency — Tight loops over contiguous memory ranges improve CPU cache utilization
- Constant overhead — Processing time depends on viewport size, not dataset size, enabling
INT_MAXitems without frame drops
Practical Implementation Examples
Basic Fixed-Height List
For the most common case where every row occupies exactly 20 pixels:
const int item_count = 1000000;
ImGuiListClipper clipper;
clipper.Begin(item_count, 20.0f);
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
ImGui::Text("Item %d", i);
}
}
Implementation reference: Begin at lines 3327–3332, Step at lines 3573–3575 in imgui.cpp.
Streaming Data with Unknown Count
When fetching data from external sources where the total isn't known upfront:
ImGuiListClipper clipper;
clipper.Begin(INT_MAX, 18.0f); // Height known, count unknown
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
const MyData* d = GetItem(i); // Database/network fetch
if (!d) break; // End of available data
ImGui::BulletText("%s", d->name);
}
// Update clipper with final count when stream ends
if (streaming_complete)
clipper.IncludeItemsByIndex(0, current_index);
}
Reference: IncludeItemsByIndex implementation at lines 89–96.
Variable-Height Rows
When items have unpredictable heights (wrapped text, varying content):
ImGuiListClipper clipper;
clipper.Begin(item_count, 0.0f); // Measure automatically
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
ImGui::TextWrapped("Variable content for item %d...", i);
}
}
The clipper handles measurement during the first Step() call (see logic at lines 35–49).
Table Integration with Custom Flags
When using clipper inside ImGui::BeginTable with custom row heights:
if (ImGui::BeginTable("my_table", 3))
{
ImGuiListClipper clipper;
clipper.Begin(item_count, row_height);
clipper.Flags |= ImGuiListClipperFlags_NoSetTableRowCounters;
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::Text("Row %d", i);
ImGui::TableNextColumn(); ImGui::Text("Data A");
ImGui::TableNextColumn(); ImGui::Text("Data B");
}
}
ImGui::EndTable();
}
Summary
- Use
ImGuiListClipperwhenever displaying more than a few hundred items to maintain frame rate - Provide fixed heights when possible to skip the measurement step and reduce overhead
- Call
IncludeItemsByIndexfor streaming or paginated data where the total count changes dynamically - Set
NoSetTableRowCounterswhen using custom row heights inside tables to avoid internal counter corruption - Reference the source in
imgui.cpp(lines 3327–3332 forBegin, lines 3573–3575 forStep) for implementation details
Frequently Asked Questions
How does ImGuiListClipper handle items with different heights?
Pass 0.0f as the items_height parameter to Begin(). During the first invocation of Step(), the clipper automatically measures the height of the first visible item and uses that for subsequent calculations. See the implementation in imgui.cpp lines 35–49 for the auto-measurement logic.
Can I use ImGuiListClipper with tables that have millions of rows?
Yes. Wrap your table row generation inside the clipper loop and consider setting the ImGuiListClipperFlags_NoSetTableRowCounters flag if your item height differs from the table's default row height. This prevents internal row counter corruption while maintaining the performance benefits of clipping.
What happens if I don't know the total item count upfront?
Call Begin(INT_MAX, item_height) with the maximum integer value, then use IncludeItemsByIndex(start, end) to inform the clipper about available data ranges as they become available. This approach supports streaming datasets and virtualized lists without pre-fetching the entire dataset.
Does using the clipper affect keyboard navigation or scrolling?
No. The clipper only optimizes CPU usage by skipping invisible items during the widget submission phase. Scrollbar behavior, scroll position, and navigation remain unchanged because End() positions the cursor at the proper final offset, maintaining the total content size.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →