JavaFX Avancé

Maîtriser les Mécanismes Internes et l'Optimisation de JavaFX

Plongez dans les rouages avancés de JavaFX pour déboguer efficacement, optimiser les performances et maîtriser les pièges courants des applications desktop professionnelles. Un cours axé sur la compréhension profonde du framework et ses mécanismes sous-jacents.

Preparetoi.academy 30 min

1. Architecture Interne du Scene Graph et Rendu Optimisé

Contexte

Le Scene Graph de JavaFX est une structure hiérarchique de nœuds qui représente l'interface utilisateur. Contrairement à Swing qui utilise un modèle de peinture immédiate, JavaFX utilise un système de graphe de scène avec rendu optimisé par le moteur graphique Prism. Comprendre cette architecture est crucial pour écrire des applications performantes et diagnostiquer les problèmes de rendu ou de latence.

Le framework JavaFX utilise plusieurs threads : le thread JavaFX (EDT), le thread de rendu Prism, et potentiellement des threads pour les tâches I/O. Les modifications du Scene Graph doivent obligatoirement se faire sur le thread JavaFX pour éviter les race conditions et les corruptions d'état.

Bloc Code Commenté

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.animation.AnimationTimer;

/**
 * Démonstration des optimisations du Scene Graph
 * et du monitoring de la performance de rendu
 */
public class SceneGraphOptimization extends Application {
    
    private long frameCount = 0;
    private long lastTime = 0;
    private Pane root;
    
    @Override
    public void start(Stage primaryStage) {
        root = new Pane();
        Scene scene = new Scene(root, 800, 600);
        
        // Création d'un grand nombre de nœuds
        // Les nœuds hors écran ne sont pas rendus grâce au culling
        for (int i = 0; i < 5000; i++) {
            Rectangle rect = new Rectangle(10, 10);
            rect.setLayoutX(Math.random() * 2000);
            rect.setLayoutY(Math.random() * 2000);
            // Le cache améliore les performances pour les sous-arbres complexes
            rect.setCache(false); // À true pour les nœuds statiques complexes
            root.getChildren().add(rect);
        }
        
        // AnimationTimer s'exécute sur le thread JavaFX à ~60 FPS
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                // Le timestamp 'now' est en nanosecondes
                if (lastTime == 0) lastTime = now;
                
                frameCount++;
                long elapsed = now - lastTime;
                
                // Calcul du FPS tous les 1 secondes (1e9 nanosecondes)
                if (elapsed > 1_000_000_000) {
                    System.out.println("FPS: " + frameCount);
                    frameCount = 0;
                    lastTime = now;
                }
            }
        };
        timer.start();
        
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Cas Réel

Une application financière affichait 10 000 cellules de tableau avec des mises à jour graphiques en temps réel. Le rendu était saccadé (30 FPS au lieu de 60). L'analyse a révélé que chaque cellule était un nœud complexe avec plusieurs enfants, et la propriété cache était désactivée. En activant le cache sur les cellules statiques et en utilisant des virtualisations via TableView, les performances ont grimpé à 58 FPS. Le culling automatique du Scene Graph a également réduit la charge de rendu en masquant les cellules non visibles.

Astuce Expert

Utilisez le paramètre JVM -Djavafx.verbose=true -Djavafx.animation=true pour obtenir des logs détaillés sur le thread de rendu. Activez le profiler avec com.sun.javafx.perf.PerformanceTracker pour mesurer les timings de rendu individuels. Pour les nœuds complexes et statiques (graphes, icônes vectorielles), activez le cache avec setCache(true) et utilisez setCacheHint(CacheHint.SPEED).


2. Gestion Avancée des Propriétés Observables et du Binding

Contexte

Le système de propriétés observables de JavaFX est au cœur de la réactivité de l'interface. Les ObservableValue et les bindings bidirectionnels créent des graphes de dépendances qui peuvent devenir complexes et générer des fuites mémoire ou des cycles infinis si mal utilisés. Maîtriser ces mécanismes est essentiel pour éviter les bugs subtils et les performances dégradées.

Les propriétés observables utilisent un système d'invalidation lazy qui ne recalcule les valeurs que si elles sont effectivement observées. Comprendre quand une propriété se marque comme invalide et quand elle se recalcule permet d'optimiser les dépendances.

Bloc Code Commenté

import javafx.beans.property.*;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.beans.binding.ObjectBinding;

/**
 * Démonstration des pièges et optimisations des observables
 */
public class AdvancedObservables {
    
    static class ViewModel {
        // Propriété simple avec invalidation lazy
        private DoubleProperty price = new SimpleDoubleProperty(100);
        private IntegerProperty quantity = new SimpleIntegerProperty(5);
        
        // Binding complexe qui recalcule seulement si observé
        private DoubleProperty total;
        
        // ObservableList pour les collections réactives
        private ObservableList<Item> items = FXCollections.observableArrayList();
        
        public ViewModel() {
            // Création d'un binding personnalisé avec gestion manuelle d'invalidation
            total = new SimpleDoubleProperty();
            
            // ChangeListener s'exécute même si l'observable n'est pas visible
            // Peut causer des fuites mémoire si non désenregistré
            ChangeListener<Number> priceListener = (obs, oldVal, newVal) -> {
                total.set(price.get() * quantity.get());
            };
            
            price.addListener(priceListener);
            quantity.addListener(priceListener);
            
            // Alternative plus performante : binding avec lazy evaluation
            // Ce binding ne recalcule que si quelqu'un l'observe
            DoubleProperty totalOptimized = new SimpleDoubleProperty() {
                private boolean valid = false;
                
                @Override
                public double get() {
                    if (!valid) {
                        set(price.get() * quantity.get());
                        valid = true;
                    }
                    return super.get();
                }
            };
            
            price.addListener((obs, old, newVal) -> {
                // Invalide le cache
                ((SimpleDoubleProperty) totalOptimized).set(0);
            });
            
            // Utilisation de Bindings.createXxxBinding pour les cas complexes
            ObjectBinding<Double> advancedBinding = Bindings.createObjectBinding(
                () -> {
                    // Ce lambda s'exécute dans le contexte du binding
                    // Les dépendances implicites sont détectées
                    return price.get() * quantity.get() * 1.2; // TVA 20%
                },
                price,      // Dépendance explicite
                quantity    // Dépendance explicite
            );
        }
        
        /**
         * Démonstration d'un piège : cycle infini
         */
        public void demonstrateCycleTrap() {
            DoubleProperty a = new SimpleDoubleProperty(10);
            DoubleProperty b = new SimpleDoubleProperty(20);
            
            // ❌ PIÈGE : cycle de binding bidirectionnel non contrôlé
            // a.bindBidirectional(b); // Éviter sans gestion d'état
            
            // ✅ SOLUTION : binding unidirectionnel + listener pour la rétroaction
            a.bind(b);
            
            // Listener pour répercuter les changements avec logique métier
            b.addListener((obs, oldVal, newVal) -> {
                if (newVal.doubleValue() != a.get()) {
                    a.unbind();
                    a.set(newVal.doubleValue());
                    a.bind(b);
                }
            });
        }
        
        /**
         * Gestion appropriée de la mémoire pour les observables
         */
        public void properMemoryManagement() {
            DoubleProperty property = new SimpleDoubleProperty();
            
            // Création d'une référence faible pour éviter les fuites mémoire
            ChangeListener<Number> listener = (obs, oldVal, newVal) -> {
                System.out.println("Valeur changée: " + newVal);
            };
            
            property.addListener(listener);
            
            // Important : désenregistrer les listeners quand l'observable n'est plus nécessaire
            // Sinon : fuite mémoire car la propriété maintient une référence forte au listener
            property.removeListener(listener);
        }
        
        public DoubleProperty priceProperty() {
            return price;
        }
        
        public IntegerProperty quantityProperty() {
            return quantity;
        }
        
        public DoubleProperty totalProperty() {
            return total;
        }
        
        public ObservableList<Item> getItems() {
            return items;
        }
    }
    
    static class Item {
        private StringProperty name = new SimpleStringProperty();
        private DoubleProperty price = new SimpleDoubleProperty();
        
        public Item(String name, double price) {
            this.name.set(name);
            this.price.set(price);
        }
        
        public StringProperty nameProperty() {
            return name;
        }
        
        public DoubleProperty priceProperty() {
            return price;
        }
    }
    
    public static void main(String[] args) {
        ViewModel vm = new ViewModel();
        
        // Ajout d'items avec observation réactive
        vm.getItems().addListener((javafx.collections.ListChangeListener<Item>) change -> {
            while (change.next()) {
                if (change.wasAdded()) {
                    System.out.println("Items ajoutés: " + change.getAddedSubList().size());
                }
            }
        });
        
        vm.getItems().add(new Item("Produit 1", 50));
        vm.getItems().add(new Item("Produit 2", 75));
    }
}

Cas Réel

Une application de gestion de portefeuille créait des bindings complexes entre prix d'actions, valeurs de portefeuille et graphiques. Avec 500 actions et des mises à jour à la seconde, le système créait des cycles de recalcul infini. L'analyse a montré que certains listeners n'étaient jamais désenregistrés (fuites mémoire), et que les bindings circulaires causaient des recalculs en cascade. La solution : utiliser des bindings unidirectionnels avec des écouteurs de changement explicites, implémenter une invalidation lazy et s'assurer que les listeners sont désenregistrés lors de la destruction des contrôles.

Astuce Expert

Utilisez l'annotation @Deprecated et le pattern WeakListener pour les listeners à longue vie. Préférez les bindings simples avec Bindings.createXxxBinding() aux listeners manuels car ils gèrent mieux les cycles. Pour déboguer les bindings, activez les logs avec System.setProperty("javafx.binding.debug", "true") qui affichera les cycles détectés.


3. Threading, Concurrence et Synchronisation dans JavaFX

Contexte

JavaFX, comme Swing, n'est pas thread-safe. Toutes les modifications du Scene Graph doivent s'effectuer sur le thread JavaFX (Application Thread / EDT). Les violations de cette règle provoquent des comportements imprévisibles : crash, corruptions d'état, interfaces figées. Maîtriser la concurrence est vital pour les applications avec opérations I/O, computations lourdes ou intégrations multi-systèmes.

Le framework fournit plusieurs mécanismes : Platform.runLater(), Task, Service et le système de binding qui synchronise automatiquement les mises à jour. Comprendre leurs différences et leurs implications de performance est crucial.

Bloc Code Commenté

import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.concurrent.Service;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 * Démonstration des patterns de concurrence sécurisés en JavaFX
 */
public class ConcurrencyPatterns extends Application {
    
    private Label statusLabel = new Label("Prêt");
    private ProgressBar progressBar = new ProgressBar(0);
    
    @Override
    public void start(Stage primaryStage) {
        VBox root = new VBox(10);
        root.getChildren().addAll(statusLabel, progressBar);
        
        // ❌ ANTIPATTERN : modification du Scene Graph depuis un autre thread
        Button badButton = new Button("Mauvaise Approche");
        badButton.setOnAction(e -> {
            new Thread(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                // ERREUR : IllegalStateException ou corruption d'état
                // statusLabel.setText("Erreur!");
            }).start();
        });
        
        // ✅ BON PATTERN 1 : Platform.runLater pour des opérations simples
        Button goodButton1 = new Button("Bonne Approche 1: Platform.runLater");
        goodButton1.setOnAction(e -> {
            new Thread(() -> {
                try {
                    // Opération longue sur thread de travail
                    Thread.sleep(2000);
                    
                    // Retour sur le thread JavaFX pour modifier l'UI
                    Platform.runLater(() -> {
                        statusLabel.setText("Opération terminée!");
                    });
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }).start();
        });
        
        // ✅ BON PATTERN 2 : Task pour les opérations avec progression
        Button goodButton2 = new Button("Bonne Approche 2: Task");
        goodButton2.setOnAction(e -> {
            Task<String> task = new Task<String>() {
                @Override
                protected String call() throws Exception {
                    // Exécuté sur un thread de work
                    for (int i = 0; i <= 100; i++) {
                        Thread.sleep(50);
                        
                        // updateProgress est thread-safe et court-circuite vers runLater
                        updateProgress(i, 100);
                        updateMessage("Progression: " + i + "%");
                    }
                    return "Résultat final";
                }
            };
            
            // Binding automatique sur le thread JavaFX
            progressBar.progressProperty().bind(task.progressProperty());
            statusLabel.textProperty().bind(task.messageProperty());
            
            // Gestionnaires de succès/échec
            task.setOnSucceeded(evt -> {
                statusLabel.setText("Succès: " + task.getValue());
                progressBar.progressProperty().unbind();
            });
            
            task.setOnFailed(evt -> {
                statusLabel.setText("Erreur: " + task.getException().getMessage());
                progressBar.progressProperty().unbind();
            });
            
            // Lance la task sur un thread de pool
            new Thread(task).start();
        });
        
        // ✅ BON PATTERN 3 : Service pour les opérations répétitives
        Button goodButton3 = new Button("Bonne Approche 3: Service");
        goodButton3.setOnAction(e -> {
            MyService service = new MyService();
            
            service.setOnSucceeded(evt -> {
                statusLabel.setText("Service résultat: " + service.getValue());
            });
            
            service.setOnFailed(evt -> {
                statusLabel.setText("Erreur service: " + service.getException().getMessage());
            });
            
            // Démarrage du service
            // restart() relance le service, idéal pour les polls réguliers
            service.restart();
        });
        
        root.getChildren().addAll(badButton, goodButton1, goodButton2, goodButton3);
        
        Scene scene = new Scene(root, 600, 400);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Concurrence JavaFX");
        primaryStage.show();
    }
    
    /**
     * Service réutilisable pour les opérations répétitives
     * Gère automatiquement le threading et les transitions d'état
     */
    static class MyService extends Service<String> {
        @Override
        protected Task<String> createTask() {
            return new Task<String>() {
                @Override
                protected String call() throws Exception {
                    // Opération simulating un appel réseau
                    Thread.sleep(2000);
                    return "Données du serveur: " + System.currentTimeMillis();
                }
            };
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

/**
 * Patterns avancés de synchronisation
 */
class AdvancedConcurrencyPatterns {
    
    /**
     * Utilisation d'un ExecutorService pour contrôler la concurrence
     */
    public static void managedThreadPool() {
        java.util.concurrent.ExecutorService executor = 
            java.util.concurrent.Executors.newFixedThreadPool(4);
        
        // Soumettre des tasks avec contrôle de ressources
        executor.submit(() -> {
            // Travail
            String result = "résultat";
            
            // Retour au thread JavaFX
            Platform.runLater(() -> {
                // Mise à jour UI
            });
        });
        
        // Important : arrêter le pool
        executor.shutdown();
    }
    
    /**
     * Double-checked locking pour l'initialisation thread-safe
     */
    static class SingletonResource {
        private static volatile SingletonResource instance;
        
        public static SingletonResource getInstance() {
            if (instance == null) {
                synchronized (SingletonResource.class) {
                    if (instance == null) {
                        instance = new SingletonResource();
                    }
                }
            }
            return instance;
        }
    }
    
    /**
     * Utilisation de CountDownLatch pour synchroniser les threads
     */
    public static void synchronizeMultipleOperations() {
        java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(3);
        
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    // Opération longue
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        
        try {
            latch.await(); // Attend que tous les threads se terminent
            Platform.runLater(() -> {
                // Tous les threads sont terminés, mise à jour UI
            });
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Cas Réel

Une application de trading recevait des mises à jour de prix via WebSocket sur un thread de connexion. Les développeurs mettaient à jour directement les labels du UI provoquant des corruptions aléatoires et des crashes. La solution : implémenter une queue thread-safe qui accumule les mises à jour et utiliser Platform.runLater() pour les appliquer par batch au thread JavaFX. Avec 1000 mises à jour/seconde, cela a réduit les appels à runLater et amélioré la stabilité. L'utilisation de Task avec updateProgress pour les longues opérations d'initialisation a également permis d'afficher une barre de progression sans bloquer l'UI.

Astuce Expert

Utilisez Task plutôt que Platform.runLater() pour les opérations avec état mutable ou progression. Les Service sont parfaites pour les opérations répétitives (polls serveur). Pour déboguer les violations du thread JavaFX, activez le vérificateur avec -Djavafx.threadingcheck=true qui lève une exception si le code s'exécute sur le mauvais thread. Utilisez jconsole pour monitorer les threads et identifier les deadlocks.


4. Optimisation du Rendu, du Cache et du Layout

Contexte

Le système de layout de JavaFX utilise un algorithme multi-passe qui peut devenir très coûteux avec des hiérarchies profondes ou des panneaux complexes. Chaque modification de taille ou position déclenche un recalcul du layout. Le système de cache permet d'échanges la mémoire contre la performance en pré-rendant les sous-arbres. Maîtriser ces mécanismes est essentiel pour les interfaces complexes avec centaines de contrôles.

JavaFX offre également des indications de cache via CacheHint et des optimisations de rendu comme effectiveNodeOrientation, l'utilisation de formes primitives au lieu de dégradés, et la réduction des appels de style CSS lors du rendu.

Bloc Code Commenté

import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

/**
 * Optimisations avancées du layout et du rendu
 */
public class RenderingOptimization extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        VBox root = new VBox(10);
        
        // ❌ ANTIPATTERN : Hiérarchie profonde avec effets coûteux
        VBox expensivePanel = createExpensivePanel();
        
        // ✅ OPTIMISATION 1 : Utiliser le cache pour les sous-arbres complexes statiques
        VBox cachedPanel = createCachedPanel();
        
        // ✅ OPTIMISATION 2 : Réduire la profondeur du layout avec des conteneurs appropriés
        VBox optimizedPanel = createOptimizedPanel();
        
        // ✅ OPTIMISATION 3 : Virtualisation pour les grandes listes
        ScrollPane virtualizedPanel = createVirtualizedPanel();
        
        root.getChildren().addAll(expensivePanel, cachedPanel, optimizedPanel, virtualizedPanel);
        
        // Monitoring du rendu
        Scene scene = new Scene(root, 1200, 800);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Optimisation Rendu");
        primaryStage.show();
        
        monitorFrameRate();
    }
    
    /**
     * Créer une hiérarchie coûteuse à rendu
     */
    private VBox createExpensivePanel() {
        VBox panel = new VBox(5);
        panel.setStyle("-fx-border-color: red; -fx-border-width: 2;");
        
        // Problème 1 : Trop d'effets GPU coûteux
        // Chaque nœud a un shadow = N appels GPU
        for (int i = 0; i < 100; i++) {
            Circle circle = new Circle(15);
            circle.setFill(Color.BLUE);
            
            // ❌ DROP SHADOW EST TRÈS COÛTEUX EN GPU
            DropShadow shadow = new DropShadow();
            circle.setEffect(shadow);
            
            panel.getChildren().add(circle);
        }
        
        return panel;
    }
    
    /**
     * Utilisation intelligente du cache
     */
    private VBox createCachedPanel() {
        VBox outerPanel = new VBox(5);
        outerPanel.setStyle("-fx-border-color: green; -fx-border-width: 2;");
        
        // Créer un sous-arbre complexe et statique
        VBox innerPanel = new VBox(5);
        for (int i = 0; i < 50; i++) {
            Rectangle rect = new Rectangle(100, 20);
            rect.setFill(Color.rgb((int)(Math.random()*255), 
                                    (int)(Math.random()*255), 
                                    (int)(Math.random()*255)));
            innerPanel.getChildren().add(rect);
        }
        
        // ✅ ACTIVER LE CACHE : pré-rend le sous-arbre en bitmap
        innerPanel.setCache(true);
        
        // Hint pour spécifier le type d'optimisation
        // SPEED : optimise pour les animations rapides (consomme plus de RAM)
        // QUALITY : optimise la qualité (plus lent)
        // ROTATE : optimise pour la rotation
        innerPanel.setCacheHint(javafx.scene.CacheHint.SPEED);
        
        outerPanel.getChildren().add(innerPanel);
        
        return outerPanel;
    }
    
    /**
     * Layout optimisé avec moins de recalculs
     */
    private VBox createOptimizedPanel() {
        VBox panel = new VBox(5);
        panel.setStyle("-fx-border-color: blue; -fx-border-width: 2;");
        
        HBox row1 = new HBox(5);
        HBox row2 = new HBox(5);
        
        // ✅ Utiliser des tailles fixes pour éviter les recalculs de layout
        for (int i = 0; i < 5; i++) {
            Rectangle rect = new Rectangle(80, 30); // Tailles fixes
            rect.setFill(Color.LIGHTBLUE);
            row1.getChildren().add(rect);
        }
        
        for (int i = 0; i < 5; i++) {
            Rectangle rect = new Rectangle(80, 30);
            rect.setFill(Color.LIGHTGREEN);
            
            // ✅ Utiliser HBox.setHgrow plutôt que des tailles préférées variables
            HBox.setHgrow(rect, Priority.ALWAYS);
            row2.getChildren().add(rect);
        }
        
        panel.getChildren().addAll(row1, row2);
        
        return panel;
    }
    
    /**
     * Virtualisation pour les grandes listes
     */
    private ScrollPane createVirtualizedPanel() {
        // ✅ ListView utilise la virtualisation par défaut
        // Seul les éléments visibles sont rendus
        javafx.scene.control.ListView<String> listView = 
            new javafx.scene.control.ListView<>();
        
        // Peuplement avec 10 000 éléments
        javafx.collections.ObservableList<String> items = 
            javafx.collections.FXCollections.observableArrayList();
        for (int i = 0; i < 10_000; i++) {
            items.add("Élément " + i);
        }
        listView.setItems(items);
        
        // ✅ Utiliser setCellFactory pour customizer le rendu
        listView.setCellFactory(param -> new javafx.scene.control.ListCell<String>() {
            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                if (empty || item == null) {
                    setText(null);
                    setGraphic(null);
                } else {
                    setText(item);
                    // Créer le graphic une seule fois, le réutiliser
                    // plutôt que de le recréer à chaque appel
                }
            }
        });
        
        ScrollPane pane = new ScrollPane(listView);
        pane.setStyle("-fx-border-color: orange; -fx-border-width: 2;");
        
        return pane;
    }
    
    /**
     * Monitoring de la fréquence de rendu et des problèmes de performance
     */
    private void monitorFrameRate() {
        javafx.animation.AnimationTimer timer = new javafx.animation.AnimationTimer() {
            private long lastTime = 0;
            private int frameCount = 0;
            
            @Override
            public void handle(long now) {
                if (lastTime == 0) lastTime = now;
                
                frameCount++;
                long elapsed = now - lastTime;
                
                // Affichage du FPS et du temps de frame
                if (elapsed > 1_000_000_000) { // 1 seconde
                    System.out.printf("FPS: %d, Temps moyen par frame: %.2f ms%n", 
                        frameCount, 
                        elapsed / frameCount / 1_000_000.0);
                    frameCount = 0;
                    lastTime = now;
                }
            }
        };
        timer.start();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

/**
 * Patterns avancés d'optimisation du layout
 */
class AdvancedLayoutOptimization {
    
    /**
     * Contrôle fin du layout : utiliser setLayoutX/setLayoutY
     * plutôt que des conteneurs pour les positions absolues
     */
    public static javafx.scene.layout.Pane createAbsoluteLayout() {
        javafx.scene.layout.Pane pane = new javafx.scene.layout.Pane();
        
        javafx.scene.shape.Rectangle rect1 = new javafx.scene.shape.Rectangle(100, 100);
        rect1.setFill(javafx.scene.paint.Color.RED);
        rect1.setLayoutX(50);   // ✅ Position absolue sans recalcul de layout
        rect1.setLayoutY(50);
        
        javafx.scene.shape.Rectangle rect2 = new javafx.scene.shape.Rectangle(100, 100);
        rect2.setFill(javafx.scene.paint.Color.BLUE);
        rect2.setLayout
Accédez à des centaines d'examens QCM — Découvrir les offres Premium