Immediate-mode vs Retained-mode UI : les deux paradigmes qui façonnent tout ce que tu utilises
De microui à Qt, comprendre les deux façons de construire une interface

Y a quelques jours, j’ai tombé sur microui. Un fichier .c de 1100 lignes. Pas de malloc. Pas d’arbre d’objets. Pas de callbacks. Pas de dépendances. Juste du ANSI C pur et dur.
Et le truc qui m’a tué, c’est ça :
C’est tout. Pas de QPushButton *btn = new QPushButton("Click"). Pas de connect(btn, &QPushButton::clicked, this, &handler). Pas de btn->setText("Nouveau label"). Pas de btn->destroy().
Le bouton “existe” juste pendant l’appel de la fonction. Et si tu cliques dessus, la fonction retourne 1. Sinon, 0. Point.
J’ai commencé à creuser. Et j’ai réalisé que pour comprendre microui, faut comprendre qu’il existe deux paradigmes radicalement différents pour construire des interfaces. Deux façons de penser le UI. Deux philosophies qui s’affrontent depuis 30 ans.
Et la plupart des devs n’ont jamais entendu parler de la différence.
Lexique
C’est là mes définitions et si elles vous déplaisent, cassez-vous je suis pas là pour discuter lexique.
- Immediate-mode UI: Paradigme où l’UI est reconstruite à chaque frame. Pas d’objets persistants. L’application décrit ce qu’elle veut dessiner, et la lib génère des commandes.
- Retained-mode UI: Paradigme où les éléments UI persistent en mémoire comme un arbre d’objets. L’application modifie l’état existant au lieu de tout redessiner.
- Widget: Composant UI — bouton, slider, textbox, fenêtre.
- Frame: Une itération de la boucle principale. À 60fps, c’est 16ms par frame.
- State synchronization: Le problème de garder l’état de l’UI en sync avec les données de l’application.
- Dirty flag: Mécanisme retained mode — marquer un élément comme “à redessiner” pour éviter de tout re-render.
- Arbre DOM: Structure arborescente des éléments UI en retained mode (comme le DOM web).
- Command list: Liste de commandes de dessin générées par un immediate-mode UI (draw rect, draw text, clip).
La découverte : microui
Bon, revenons à microui. Pourquoi c’est intéressant ?
Parce que c’est la version la plus pure de l’immediate-mode UI que tu puisses trouver. 1100 lignes de C. Zéro allocation dynamique. Tout vit dans un mu_Context alloué par l’utilisateur. Pas de new, pas de malloc, pas de free.
L’architecture est d’une simplicité dérangeante :
- Tu passes l’input (souris, clavier) à la lib
- Tu appelles
mu_begin() - Tu dessines tes contrôles (
mu_begin_window,mu_button,mu_slider…) - Tu appelles
mu_end() - La lib te donne une liste de commandes (rectangles, texte, icônes)
- Tu les rends avec ton propre backend (OpenGL, Vulkan, software)
Et le plus beau : l’UI n’existe pas entre les frames. Il n’y a pas d’objet Button qui persiste en mémoire. Pas de widget->visible = true. Pas de container->addChild(btn).
Le bouton est une function call. Tu l’appelles, il dessine, il vérifie l’input, il retourne un résultat. C’est tout.
0if (mu_begin_window(ctx, "My Window", mu_rect(10, 10, 300, 400))) {
1 mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
2
3 mu_label(ctx, "First:");
4 if (mu_button(ctx, "Button1")) {
5 printf("Button1 pressed\n");
6 }
7
8 mu_label(ctx, "Second:");
9 if (mu_button(ctx, "Button2")) {
10 mu_open_popup(ctx, "My Popup");
11 }
12
13 mu_end_window(ctx);
14}C’est du procédural. C’est comme dessiner sur un canvas. Tu dis “ici un bouton, ici un label, ici un slider”. Et la lib fait le reste.
Mais pour comprendre pourquoi c’est révolutionnaire, faut comprendre ce que ça remplace.
Retained-mode UI : le paradigme dominant
Définition
En retained-mode UI, les éléments UI sont créés et persistent en mémoire comme un arbre d’objets. L’application modifie l’état existant au lieu de tout redessiner.
C’est le paradigme de Qt, GTK, WPF, Swing, WinAPI, et le DOM web. C’est celui que 99% des devs utilisent sans le savoir.
Exemple concret
En Qt, un bouton :
0// Création (une fois)
1QPushButton *btn = new QPushButton("Click");
2connect(btn, &QPushButton::clicked, this, &MyClass::handler);
3layout->addWidget(btn);
4
5// Modification (plus tard)
6btn->setText("Nouveau label");
7btn->setVisible(false);En JavaScript (DOM web) :
0// Création (une fois)
1const btn = document.createElement('button');
2btn.textContent = 'Click';
3btn.addEventListener('click', handler);
4document.body.appendChild(btn);
5
6// Modification (plus tard)
7btn.textContent = 'Nouveau label';
8btn.style.display = 'none';Le bouton existe en mémoire. Il a un état (texte, visibility, position). Il vie tant qu’on le détruit pas explicitement.
Comment ça marche
L’arbre d’objets UI est maintenu en mémoire. Quand un événement arrive (clic, clavier), la lib le dispatch via l’arbre — les callbacks se propagent (bubble up) ou se propagent vers le bas (capture).
Quand tu modifies un élément, la lib le marque comme “dirty” (sale). Au prochain render pass, elle ne redessine que les éléments marqués. C’est efficace.
Arbre UI en mémoire :
┌─────────────────────┐
│ Window │
│ ├── Panel │
│ │ ├── Label │
│ │ ├── Button (dirty)
│ │ └── TextBox │
│ └── ScrollBar │
└─────────────────────┘
Les gros avantages
Efficacité CPU : Seuls les éléments modifiés sont redessinés. Si rien ne change, rien n’est redessiné. C’est le dirty flag en action.
Animations : L’objet existe entre les frames. Tu peux interpoler sa position, sa couleur, sa taille. C’est trivial.
Écosystème riche : Qt, GTK, WPF, DOM — des milliers de composants, des docs complètes, une communauté massive.
Accessibilité : Les screen readers, le keyboard navigation, les contrastes — tout ça fonctionne parce que l’arbre d’objets existe et est inspectable.
Native look : Les widgets ressemblent à ce que le système d’exploitation attend.
Les gros défauts
State synchronization : Tu as deux copies de la vérité — ton data model et ton UI tree. Quand l’une change, faut sync l’autre. C’est la source de 80% des bugs UI.
0// Le classique : le data model change mais l'UI est pas au courant
1user->name = "Nouveau nom";
2// Oubli de : nameLabel->setText(user->name);
3// Résultat : l'UI affiche l'ancien nom. Bug silencieux.
Boilerplate : Créer, configurer, attacher, détruire des objets. Le lifecycle est une source de complexity infinie.
Stale state : Un widget référence un objet détruit. Crash. Segfault. Le genre de bug qui te fait perdre une journée.
Complexité : Event propagation, lifecycle management, memory leaks, signal/slot connections. C’est un monde.
Immediate-mode UI : le paradigme radical
Définition
En immediate-mode UI, l’UI est reconstruite à chaque frame. Pas d’objets persistants. L’application décrit procéduralement ce qu’elle veut dessiner, et la lib génère une liste de commandes.
C’est le paradigme de Dear ImGui, microui, Nuklear. C’est celui que les game devs et les outils de debug utilisent depuis 20 ans.
Exemple concret
En Dear ImGui :
0// Chaque frame (pas de création, pas de destruction)
1static bool show_demo = true;
2static float value = 0.5f;
3
4ImGui::Begin("My Window");
5if (ImGui::Button("Click")) {
6 printf("Pressed!\n");
7}
8ImGui::SliderFloat("Value", &value, 0.0f, 1.0f);
9ImGui::End();En microui :
0// Chaque frame
1if (mu_begin_window(ctx, "My Window", mu_rect(10, 10, 300, 400))) {
2 mu_layout_row(ctx, 1, (int[]) { -1 }, 0);
3 if (mu_button(ctx, "Click")) {
4 printf("Pressed!\n");
5 }
6 mu_slider(ctx, &value, 0.0f, 1.0f);
7 mu_end_window(ctx);
8}Le bouton n’existe pas en mémoire. Il est dessiné, il vérifie l’input, il retourne un résultat. C’est une function call, pas un objet.
Comment ça marche
- L’application décrit l’UI procéduralement
- La lib génère une command list (draw rect, draw text, set clip)
- Le backend les rend (OpenGL, Vulkan, software)
- L’état est possédé par l’application, pas par la lib
Flux d'une frame :
App → "Dessine un bouton ici" → Lib → Command List → Backend → Écran
↑ ↑
Pas d'objet persistant Juste des commandes
Les gros avantages
Zéro state synchronization : L’application possède l’état. Point. Pas de duplication. Pas de bug “l’UI et le data model sont pas en sync”.
0// L'état vit ici, dans ton code. Pas dans la lib.
1static char name[64] = "Default";
2
3// L'UI lit et écrit directement ton état
4ImGui::InputText("Name", name, sizeof(name));
5// Pas de nameLabel->setText(). Pas de sync. C'est fait.
Code ultra-simple : Pas de lifecycle. Pas de callbacks. Pas de new/delete. Pas de connect(). Juste des function calls.
Layout dynamique : L’UI change facilement. Tu veux un bouton conditionnel ? if (condition) { mu_button(...); }. C’est tout.
Parfait pour les outils : Debug, profiling, éditeurs, dashboards. Le genre de truc où tu veux voir des données en temps réel sans te trimballer un arbre d’objets.
Les gros défauts
CPU-bound : Tout est redessiné chaque frame. Même si rien n’a changé. Sur un écran chargé, ça peut coûter cher.
Pas d’animations faciles : L’objet n’existe pas entre les frames. Tu veux une animation de fondu ? Faudra gérer l’état toi-même.
Pas d’accessibilité : Pas de screen reader. Pas de keyboard navigation native. Pas de contraste géré. C’est un outil de dev, pas une app grand public.
Pas de native look : Les widgets ressemblent à… rien de connu. C’est un look “ImGui”, pas “Windows” ou “macOS”.
Input text basique : Pas de sélection, pas de Ctrl+A/C/V, pas de undo/redo. Le text editing est un casse-tête en immediate mode.
Le piège mental
“Immediate mode = lent”
Faux. C’est un problème d’API, pas d’implémentation. Dear ImGui ne fait pas un drawRect OpenGL pour chaque rectangle. Il génère une command list, la batch, et génère des vertex buffers optimisés. Un frame idle ne fait aucun malloc/free.
Le coût CPU vient du fait que tout est re-évalué chaque frame. Mais “re-évalué” ne veut pas dire “redessiné”. La lib peut cacher des choses en interne.
“Immediate mode = pas d’état”
Faux. La lib peut garder un état interne (et elle le fait — microui a un pool de containers, Dear ImGui a des caches de vertex buffers). C’est l’API qui est immediate, pas l’implémentation.
Comme le dit Omar Cornut (auteur de Dear ImGui) : “Immediate mode is a statement about the interface of the library, not the internals.”
“Retained mode = toujours mieux”
Faux. Pour les outils, l’Immediate mode est souvent supérieur. Pas de boilerplate, pas de state sync, feedback immédiat. C’est un choix de paradigme, pas une hiérarchie.
Tableau comparatif
| Aspect | Immediate Mode | Retained Mode |
|---|---|---|
| Cycle de vie | Tout redessiné chaque frame | Créé une fois, modifié ensuite |
| État | L’app possède l’état (source of truth) | La lib possède l’arbre d’objets |
| State sync | Naturelle (pas de duplication) | Manuelle (signals, callbacks) |
| Mémoire | Faible (pas d’arbre persistant) | Élevée (objets UI en mémoire) |
| CPU | Plus coûteux (réévalue tout) | Efficace (dirty flag) |
| Layout | Dynamique (change à chaque frame) | Statique (modifié explicitement) |
| Complexité | Simple conceptuellement | Complexe (lifecycle, events) |
| Animations | Difficiles (pas d’objet persistant) | Naturelles (l’objet existe) |
| Accessibilité | Aucune | Bonne (screen reader, keyboard) |
| Native look | Non | Oui |
| Idéal pour | Outils, debug, prototypage | Apps desktop, mobile, web |
| Exemples | Dear ImGui, microui, Nuklear | Qt, GTK, WPF, DOM, SwiftUI |
Le 3ème paradigme : Declarative UI
Et si je te disais qu’en 2026, la plupart des frameworks modernes utilisent un hybride ?
SwiftUI, Jetpack Compose, QML, React, Vue — tous décrivent l’UI de manière déclarative. Tu dis ce que tu veux, pas comment le dessiner.
0// SwiftUI — déclaratif
1struct ContentView: View {
2 @State private var count = 0
3
4 var body: some View {
5 VStack {
6 Text("Count: \(count)")
7 Button("Increment") { count += 1 }
8 }
9 }
10}La syntaxe ressemble à de l’immediate mode — tu décris l’UI à chaque frame. Mais en coulisses, le framework maintient un virtual tree (comme le DOM virtuel de React) et ne redessine que ce qui a changé via un diffing algorithm.
C’est le meilleur des deux mondes ? Pas exactement. C’est un compromis :
- Tu as la simplicité syntaxique de l’immediate mode
- Tu as la performance du retained mode
- Mais tu as aussi la complexité des deux (state management, reactivity, lifecycle)
C’est pas un 3ème paradigme. C’est un hybride. Et ça fonctionne remarquablement bien pour les apps grand public.
Mot de la fin
Il n’y a pas de “meilleur” paradigme. Y a le bon pour ton use case.
Retained mode si tu veux du polish, de l’accessibilité, du native look. Si tu fais une app desktop, mobile, ou web pour des utilisateurs finaux. Si tu veux des animations fluides et un écosystème riche.
Immediate mode si tu veux de la simplicité, de la flexibilité, du feedback immédiat. Si tu fais un outil de debug, un éditeur, un dashboard. Si tu veux zéro boilerplate et zéro state synchronization.
Declarative si tu veux les deux. Si tu veux la syntaxe propre de l’immediate mode avec la perf du retained mode. Si tu es prêt à payer le coût en complexité de reactivity.
Et si tu veux vraiment comprendre la différence, fais comme moi : télécharge microui. Compile-le. Regarde comment 1100 lignes de C font tout ce que Qt fait en 5 millions.
Et après, dis-moi si tu vois encore l’UI de la même façon.
See ya space-cowboy!