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 in imgui.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_MAX items 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 ImGuiListClipper whenever 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 IncludeItemsByIndex for streaming or paginated data where the total count changes dynamically
  • Set NoSetTableRowCounters when using custom row heights inside tables to avoid internal counter corruption
  • Reference the source in imgui.cpp (lines 3327–3332 for Begin, lines 3573–3575 for Step) 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →