How to Use ImGuiListClipper for Efficient List Rendering in Dear ImGui
ImGuiListClipper is a helper class that renders only visible items in scrolling regions, reducing O(N) iteration costs to O(V) for visible rows only.
The ImGuiListClipper class in the ocornut/imgui repository solves the performance bottleneck of rendering large datasets by skipping invisible items entirely. When displaying thousands or millions of rows, iterating through every entry to call ImGui functions creates unnecessary CPU overhead. This clipper leverages internal visibility calculations to process only the subset of items that intersect the current viewport.
Core Architecture and Source Files
The public API resides in imgui.h at lines 2894–2921, where the ImGuiListClipper struct declares the constructor, Begin(), Step(), End() methods, and the DisplayStart/DisplayEnd indices. Internal bookkeeping structures live in imgui_internal.h:
ImGuiListClipperRange(lines 1689–1699): Stores clipping ranges and seek offsetsImGuiListClipperData(lines 1702–1712): Maintains step counters and range buffers
The algorithm implementation appears in imgui.cpp, specifically within the ImGuiListClipper::Begin, Step, and End method definitions.
How ImGuiListClipper Works Internally
The clipper operates through a three-phase state machine that caches visibility results across frames:
- Begin – Initialize with
items_count(total rows) and optionalitems_height. PassINT_MAXif the count is unknown. - Step – Advance internal state and populate
DisplayStartandDisplayEndwith the indices of rows intersecting the visible scrolling region. Returnstruewhile work remains. - End – Reset internal buffers, allowing clipper reuse. Called automatically by the destructor.
The clipper handles variable item heights by refining estimates after the first frame renders, and automatically synchronizes with table row counters unless ImGuiListClipperFlags_NoSetTableRowCounters is specified.
Basic Usage Pattern
Wrap your rendering loop inside the clipper's Step() iteration:
ImGuiListClipper clipper;
clipper.Begin(items_count);
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
// Heavy per-item work only executes for visible rows
ImGui::Text("Item %d", i);
ImGui::Text("Data: %s", large_dataset[i].label);
}
}
clipper.End(); // Optional: destructor handles this
This pattern ensures expensive operations—string formatting, image loading, or complex widget calculations—execute only for approximately V visible rows rather than N total items.
Advanced Configuration Scenarios
Variable Item Heights
When rows have non-uniform heights, provide an average estimate to Begin():
// Second parameter is approximate height in pixels
clipper.Begin(items_count, 20.0f);
The clipper recalculates precise ranges after measuring actual rendered heights during the first frame.
Unknown Item Counts
For streaming data or lazily-generated lists, pass INT_MAX and break when data ends:
clipper.Begin(INT_MAX);
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
if (i >= live_data_buffer.size())
break;
ImGui::Text("Live item %d", i);
}
}
Table Integration
Inside BeginTable calls, the clipper automatically calls TableNextRow() for each visible index:
if (ImGui::BeginTable("LargeTable", 3, ImGuiTableFlags_ScrollY))
{
ImGuiListClipper clipper;
clipper.Begin(row_count);
while (clipper.Step())
{
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("%d", row);
ImGui::TableSetColumnIndex(1);
ImGui::Text("%f", data[row].value);
ImGui::TableSetColumnIndex(2);
ImGui::Text("%s", data[row].name);
}
}
ImGui::EndTable();
}
Disable automatic table row management by passing ImGuiListClipperFlags_NoSetTableRowCounters to Begin() if you need manual control over row advancement.
Summary
- ImGuiListClipper lives in
imgui.hand eliminates invisible item processing in large lists - Begin(), Step(), End() form the core API for driving the visibility algorithm
- DisplayStart/DisplayEnd provide the index range for your rendering loop
- Variable heights and tables receive automatic optimization with optional manual override flags
- INT_MAX support enables streaming data scenarios without predetermined counts
Frequently Asked Questions
What is ImGuiListClipper and why is it necessary?
ImGuiListClipper is a CPU optimization utility that calculates which rows of a scrolling list are actually visible within the current window clipping rectangle. Without it, iterating through thousands of items to call ImGui functions—even for off-screen rows—wastes significant CPU cycles on invisible geometry that gets discarded during rendering.
How does ImGuiListClipper handle items with different heights?
The clipper accepts an estimated height parameter in Begin(). During the first frame, it uses this estimate to guess the visible range, then refines the calculation after ImGui measures actual item heights. Subsequent frames use the cached measurements for accurate positioning regardless of row-to-row height variation.
Can ImGuiListClipper work with Dear ImGui tables?
Yes. When used inside BeginTable/EndTable blocks, the clipper automatically invokes TableNextRow() for each visible index, maintaining proper table row counters and clip rectangles. Use ImGuiListClipperFlags_NoSetTableRowCounters only if your code manually manages row advancement via TableNextRow().
What happens if I don't know the total number of items?
Pass INT_MAX as the item count to Begin(). The clipper will continue emitting steps until Step() returns false or you break the loop internally. This supports use cases like log tailing or infinite scroll where data arrives continuously and the total size is unknown.
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 →