# How to Optimize Dear ImGui Performance and Reduce Draw Calls: 8 Proven Techniques

> Optimize Dear ImGui performance with 8 proven techniques. Reduce draw calls by mastering UI geometry batching and leveraging ImDrawListSplitter.

- Repository: [omar/imgui](https://github.com/ocornut/imgui)
- Tags: performance
- Published: 2026-07-18

---

**Dear ImGui batches UI geometry into `ImDrawCmd` objects and automatically merges consecutive commands sharing identical clip rectangles, textures, and vertex offsets, allowing developers to minimize GPU draw calls by managing draw-list state changes and leveraging `ImDrawListSplitter` for complex layouts.**

Dear ImGui (ocornut/imgui) renders user interfaces by accumulating primitives into **ImDrawList** objects that the backend translates into GPU draw commands. Every state change—whether switching textures, modifying clip rectangles, or allocating new draw lists—forces the creation of additional draw commands that increase CPU and GPU overhead. To optimize Dear ImGui performance and reduce draw calls, you must understand the library’s internal batching mechanisms and minimize state transitions during the render loop.

## Understanding ImGui’s Draw Call Architecture

Dear ImGui stores all UI geometry in **ImDrawList** containers, each holding a sequence of **ImDrawCmd** structures. The renderer submits one GPU draw call per command, making command count the primary metric for optimization. The library implements aggressive batching strategies to keep this count minimal.

In [`imgui_draw.cpp`](https://github.com/ocornut/imgui/blob/main/imgui_draw.cpp), the method `ImDrawList::_TryMergeDrawCmds()` (lines 77-86) automatically combines consecutive draw commands when they share identical clip rectangles, texture IDs, and vertex offsets. This prevents small primitives—such as individual lines or rectangles—from generating separate GPU calls. Conversely, `ImDrawList::_OnChangedClipRect()` (lines 94-100) creates new commands only when necessary, ensuring that state changes occur strictly when clip rectangles, textures, or vertex offsets differ.

## Built-In Batching Mechanisms

### Automatic Command Merging

The `ImDrawList` class attempts to merge draw commands at submission time. When you add multiple primitives using the same texture and without clipping changes, `ImDrawList::_TryMergeDrawCmds()` collapses them into a single `ImDrawCmd`. This merge operation eliminates redundant GPU state setup and reduces driver overhead.

### Channel Splitting with ImDrawListSplitter

For complex layouts requiring out-of-order rendering, Dear ImGui provides **ImDrawListSplitter** (implemented in [`imgui_draw.cpp`](https://github.com/ocornut/imgui/blob/main/imgui_draw.cpp), lines 2151-2185). This utility splits a single draw list into multiple channels, allowing you to render columnar data or overlapping elements separately before merging them back into a unified command buffer. The tables API uses this extensively to batch per-column draws into a single draw call.

### Vertex Offset Management

When UI complexity exceeds 16-bit index limits, Dear ImGui can either split the draw list or use 32-bit indices via the `ImDrawListFlags_AllowVtxOffset` flag. During `ImGui::NewFrame()` (lines 5566-5574 of [`imgui.cpp`](https://github.com/ocornut/imgui/blob/main/imgui.cpp)), the library initializes `g.DrawListSharedData.InitialFlags` with this flag enabled by default, preventing unnecessary draw-list splits that would otherwise double your draw call count for large UIs.

## 8 Techniques to Minimize Draw Calls

**1. Batch Custom Primitives with Foreground Draw Lists**
Use `ImGui::GetForegroundDrawList()` (defined in [`imgui.cpp`](https://github.com/ocornut/imgui/blob/main/imgui.cpp), lines 5310-5318) to append custom geometry to the viewport’s existing draw list. This reuses the active command buffer rather than allocating new resources.

**2. Eliminate Texture Switches**
Each call to `ImDrawList::PushTextureID()` forces a new draw command. Keep the same texture bound—typically the default font atlas—for as many primitives as possible. If you need different font sizes, use `ImFont::Scale` or `ImGui::SetWindowFontScale()` instead of loading additional fonts that expand the atlas and trigger texture changes.

**3. Leverage ImDrawListSplitter for Custom Tables**
When building custom grid layouts, instantiate `ImDrawListSplitter`, call `Split()` before rendering columns, draw into separate channels, then invoke `Merge()`. This pattern—identical to the internal tables implementation in [`imgui_tables.cpp`](https://github.com/ocornut/imgui/blob/main/imgui_tables.cpp)—batches column geometry into a single draw command.

**4. Minimize Style Changes**
Calling `ImGui::PushStyleColor()` or `ImGui::PushStyleVar()` modifies draw-list state and forces new draw commands. Batch elements sharing the same style before applying changes, and prefer calculating colors manually when rendering raw primitives via draw lists.

**5. Prefer Child Windows Over Separate Windows**
Child windows can render backgrounds, borders, and scrollbars directly into the parent window’s draw list. As noted in `ImGui::BeginChild()` (line 8064 of [`imgui.cpp`](https://github.com/ocornut/imgui/blob/main/imgui.cpp)), this avoids creating a separate `ImDrawList` object and eliminates an entire draw call per child.

**6. Disable Anti-Aliasing When Possible**
Anti-aliased lines and fills generate additional vertices. Disable `ImDrawListFlags_AntiAliasedLines` and `ImDrawListFlags_AntiAliasedFill` in `ImGui::GetStyle()` for UI elements where jagged edges are acceptable, reducing vertex buffer pressure and driver work.

**7. Avoid Multi-Viewport Overhead**
The `ImGuiConfigFlags_ViewportsEnable` flag creates separate draw lists for each platform window. If your application does not require multiple OS-level windows, leave this disabled to maintain a single command buffer.

**8. Monitor Vertex Buffer Limits**
Ensure `ImDrawListFlags_AllowVtxOffset` remains enabled (the default) when rendering massive UIs. This flag allows a single draw list to handle vertex buffers exceeding 16-bit index limits without splitting into multiple commands.

## Practical Implementation Examples

The following example demonstrates batching rectangles, lines, and text into a single draw command by reusing the foreground draw list and maintaining constant texture state:

```cpp
void ShowCustomBatch()
{
    // Acquire the viewport's foreground draw list to avoid new allocations
    ImDrawList* dl = ImGui::GetForegroundDrawList();  // imgui.cpp#L5310-L5318
    
    // Push the default font texture once; changing textures forces new commands
    dl->PushTextureID(ImGui::GetIO().Fonts->TexID);
    
    // These three primitives share clip-rect and texture → merged by _TryMergeDrawCmds()
    dl->AddRectFilled(ImVec2(10,10), ImVec2(200,60), 
                      ImGui::GetColorU32(ImGuiCol_Button));
    dl->AddLine(ImVec2(10,10), ImVec2(200,60), 
                ImGui::GetColorU32(ImGuiCol_Text), 1.0f);
    dl->AddText(ImGui::GetFont(), ImGui::GetFontSize(), 
                ImVec2(15,15), ImGui::GetColorU32(ImGuiCol_Text), "Batch demo");
    
    dl->PopTextureID();  // Restore state without creating new commands
}

```

For complex layouts requiring columnar rendering, use the splitter API to batch draws:

```cpp
void CustomTableBatch()
{
    ImGui::Begin("Optimized Table");
    ImDrawList* dl = ImGui::GetWindowDrawList();
    
    ImDrawListSplitter splitter;
    // Split into 3 channels; imgui_draw.cpp#L2151-L2159
    splitter.Split(dl, 3);
    
    // Column 0
    splitter.SetCurrentChannel(dl, 0);
    dl->AddRectFilled(ImVec2(10,30), ImVec2(110,80), 
                      ImGui::GetColorU32(ImGuiCol_Header));
    
    // Column 1
    splitter.SetCurrentChannel(dl, 1);
    dl->AddRectFilled(ImVec2(120,30), ImVec2(220,80), 
                      ImGui::GetColorU32(ImGuiCol_Header));
    
    // Column 2
    splitter.SetCurrentChannel(dl, 2);
    dl->AddRectFilled(ImVec2(230,30), ImVec2(330,80), 
                      ImGui::GetColorU32(ImGuiCol_Header));
    
    // Merge channels into single command buffer; imgui_draw.cpp#L2181-L2188
    splitter.Merge(dl);
    ImGui::End();
}

```

## Summary

- **ImDrawList** accumulates geometry into **ImDrawCmd** objects, with `ImDrawList::_TryMergeDrawCmds()` automatically merging consecutive commands sharing identical state.
- Use `ImGui::GetForegroundDrawList()` to append custom geometry without allocating new draw lists or commands.
- Avoid texture switches and unnecessary `PushStyleColor()` calls, as both force new draw commands.
- Implement `ImDrawListSplitter` (Split/Merge) for columnar layouts to batch per-cell rendering into unified draw calls.
- Keep `ImDrawListFlags_AllowVtxOffset` enabled to prevent draw-list splits when vertex counts exceed 16-bit limits.
- Render child window decorations into parent draw lists via `ImGui::BeginChild()` to eliminate redundant command buffers.

## Frequently Asked Questions

### Does anti-aliasing increase draw calls in Dear ImGui?

Anti-aliasing does not directly increase the number of draw calls, but it increases vertex count and shader complexity. Disabling `ImDrawListFlags_AntiAliasedLines` reduces GPU workload per draw call, which improves frame times when rendering dense line-based plots or graphs. Disable these flags in the `ImGuiStyle` structure or per-draw-list when maximum performance is required.

### How can I render thousands of custom shapes without increasing draw calls?

Use `ImGui::GetForegroundDrawList()` or `ImGui::GetWindowDrawList()` to append all primitives to a single draw list, ensuring they share the same texture ID and clip rectangle. The `ImDrawList::_TryMergeDrawCmds()` function will batch them into one `ImDrawCmd`. For shapes requiring different textures, consider combining them into a texture atlas to maintain batching.

### Why does switching fonts cause additional draw calls?

Each font texture change requires `ImDrawList::PushTextureID()`, which triggers `ImDrawList::_OnChangedClipRect()` to create a new draw command. Loading multiple fonts expands the texture atlas or creates separate atlas pages, forcing texture state changes. Use `ImFont::Scale` or `ImGui::SetWindowFontScale()` to vary text size while keeping the same underlying texture ID.

### Should I use ImGui tables or manual rendering for data grids?

Use **ImGui tables** ([`imgui_tables.cpp`](https://github.com/ocornut/imgui/blob/main/imgui_tables.cpp)) for data grids. The tables API internally utilizes `ImDrawListSplitter` to batch column rendering efficiently, minimizing draw calls while providing sorting, resizing, and clipping functionality. Manual rendering with raw `ImDrawList` calls is only necessary for highly specialized visualization requirements not supported by the tables system.