Files
btpxpress-frontend/PRIMEFACES_BEST_PRACTICES_OPTIMIZATION.md

24 KiB

🚀 Optimisation BTPXpress - Meilleures Pratiques PrimeFaces

📋 Table des Matières

  1. Analyse du Projet Actuel
  2. Optimisations DataTable & Lazy Loading
  3. Performance Ajax & Partial Rendering
  4. Composants Réutilisables
  5. Gestion d'État & ViewScoped
  6. Validation & Messages
  7. Plan d'Implémentation

🔍 Analyse du Projet Actuel

Points Forts

  • Utilisation de @ViewScoped pour les beans (bonne pratique)
  • Architecture BaseListView pour la réutilisation du code
  • Séparation des concerns (Service, View, Model)
  • Utilisation de composants réutilisables (liste-table.xhtml, liste-filters.xhtml)

Points à Améliorer 🔧

  • ⚠️ Pas de Lazy Loading : Toutes les données sont chargées en mémoire
  • ⚠️ Filtrage côté client : Le filtrage se fait en Java après récupération complète
  • ⚠️ Pas de cache : Rechargement complet à chaque fois
  • ⚠️ Updates Ajax trop larges : Risque de re-rendering inutile
  • ⚠️ Pas de composants composites Freya : Utilisation directe de PrimeFaces

📊 Optimisations DataTable & Lazy Loading

1. Implémenter LazyDataModel

Problème actuel : Dans FactureView.java, toutes les factures sont chargées :

List<Map<String, Object>> facturesData = factureService.getAllFactures();

Solution : Utiliser LazyDataModel de PrimeFaces

Créer un LazyDataModel personnalisé

package dev.lions.btpxpress.view.model;

import org.primefaces.model.FilterMeta;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortMeta;
import dev.lions.btpxpress.service.FactureService;
import dev.lions.btpxpress.view.FactureView.Facture;

import java.util.*;

public class FactureLazyDataModel extends LazyDataModel<Facture> {

    private final FactureService factureService;

    public FactureLazyDataModel(FactureService factureService) {
        this.factureService = factureService;
    }

    @Override
    public int count(Map<String, FilterMeta> filterBy) {
        // Appel API pour compter le nombre total avec filtres
        return factureService.countFactures(buildFilterParams(filterBy));
    }

    @Override
    public List<Facture> load(int first, int pageSize,
                              Map<String, SortMeta> sortBy,
                              Map<String, FilterMeta> filterBy) {
        // Appel API avec pagination, tri et filtres
        Map<String, Object> params = new HashMap<>();
        params.put("offset", first);
        params.put("limit", pageSize);
        params.putAll(buildSortParams(sortBy));
        params.putAll(buildFilterParams(filterBy));

        return factureService.getFacturesLazy(params);
    }

    private Map<String, Object> buildSortParams(Map<String, SortMeta> sortBy) {
        Map<String, Object> params = new HashMap<>();
        if (sortBy != null && !sortBy.isEmpty()) {
            SortMeta sort = sortBy.values().iterator().next();
            params.put("sortField", sort.getField());
            params.put("sortOrder", sort.getOrder().name());
        }
        return params;
    }

    private Map<String, Object> buildFilterParams(Map<String, FilterMeta> filterBy) {
        Map<String, Object> params = new HashMap<>();
        if (filterBy != null) {
            filterBy.forEach((key, filter) -> {
                if (filter.getFilterValue() != null) {
                    params.put("filter_" + key, filter.getFilterValue());
                }
            });
        }
        return params;
    }
}

Modifier FactureView pour utiliser LazyDataModel

@Named("factureView")
@ViewScoped
public class FactureView implements Serializable {

    @Inject
    FactureService factureService;

    private LazyDataModel<Facture> lazyModel;

    @PostConstruct
    public void init() {
        lazyModel = new FactureLazyDataModel(factureService);
    }

    public LazyDataModel<Facture> getLazyModel() {
        return lazyModel;
    }
}

Modifier factures.xhtml pour utiliser lazy loading

<p:dataTable id="facturesTable"
             value="#{factureView.lazyModel}"
             var="facture"
             lazy="true"
             paginator="true"
             rows="10"
             rowsPerPageTemplate="10,20,50"
             paginatorPosition="both"
             paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
             currentPageReportTemplate="Affichage {startRecord}-{endRecord} sur {totalRecords}"
             filterDelay="500"
             emptyMessage="Aucune facture trouvée">

    <!-- Colonnes avec filtres intégrés -->
    <p:column headerText="Numéro"
              sortBy="#{facture.numero}"
              filterBy="#{facture.numero}"
              filterMatchMode="contains">
        <h:outputText value="#{facture.numero}"/>
    </p:column>

    <!-- ... autres colonnes ... -->
</p:dataTable>

2. Optimiser le Service Backend

Ajouter des endpoints paginés dans BtpXpressApiClient :

@Path("/api/factures")
public interface BtpXpressApiClient {

    @GET
    @Path("/lazy")
    @Produces(MediaType.APPLICATION_JSON)
    Response getFacturesLazy(
        @QueryParam("offset") int offset,
        @QueryParam("limit") int limit,
        @QueryParam("sortField") String sortField,
        @QueryParam("sortOrder") String sortOrder,
        @QueryParam("filter_numero") String filtreNumero,
        @QueryParam("filter_client") String filtreClient,
        @QueryParam("filter_statut") String filtreStatut
    );

    @GET
    @Path("/count")
    @Produces(MediaType.APPLICATION_JSON)
    int countFactures(
        @QueryParam("filter_numero") String filtreNumero,
        @QueryParam("filter_client") String filtreClient,
        @QueryParam("filter_statut") String filtreStatut
    );
}

Performance Ajax & Partial Rendering

1. Optimiser les Updates Ajax

Problème : Updates trop larges qui re-rendent des composants inutilement

Mauvaise pratique :

<p:commandButton value="Filtrer"
                 update="@form"
                 action="#{factureView.applyFilters}"/>

Bonne pratique :

<p:commandButton value="Filtrer"
                 update="facturesTable messages"
                 process="@this filtresPanel"
                 action="#{factureView.applyFilters}"/>

2. Utiliser process et update de manière ciblée

Règles d'or :

  • process : Spécifie quels composants doivent être traités (validation, conversion, update model)
  • update : Spécifie quels composants doivent être re-rendus
  • Toujours utiliser les IDs spécifiques plutôt que @form ou @all

Exemple optimisé :

<h:form id="factureForm">
    <p:panel id="filtresPanel">
        <p:inputText id="filtreNumero" value="#{factureView.filtreNumero}"/>
        <p:inputText id="filtreClient" value="#{factureView.filtreClient}"/>

        <p:commandButton value="Rechercher"
                         process="@this filtresPanel"
                         update="facturesTable messages"
                         action="#{factureView.search}"/>
    </p:panel>

    <p:messages id="messages"/>

    <p:dataTable id="facturesTable" value="#{factureView.lazyModel}" var="facture">
        <!-- colonnes -->
    </p:dataTable>
</h:form>

3. Utiliser p:ajax pour les événements

Pour les changements de filtres en temps réel :

<p:inputText id="filtreNumero" value="#{factureView.filtreNumero}">
    <p:ajax event="keyup"
            delay="500"
            update="facturesTable"
            process="@this"
            listener="#{factureView.onFilterChange}"/>
</p:inputText>

4. Désactiver les auto-updates inutiles

Éviter :

<p:dataTable autoUpdate="true">  <!-- ❌ Mauvaise pratique -->

Préférer :

<p:dataTable id="table">
    <p:ajax event="rowSelect" update="detailPanel" listener="#{bean.onRowSelect}"/>
</p:dataTable>

🧩 Composants Réutilisables

1. Migrer vers Freya Extension

Avantages :

  • Composants pré-stylés cohérents
  • Moins de code boilerplate
  • Meilleure maintenabilité

Exemple : Remplacer p:dataTable par fr:dataTable

Avant :

<p:dataTable id="facturesTable"
             value="#{factureView.items}"
             var="facture"
             paginator="true"
             rows="10"
             styleClass="p-datatable-striped"
             emptyMessage="Aucune facture">
    <p:column headerText="Numéro">
        <h:outputText value="#{facture.numero}"/>
    </p:column>
</p:dataTable>

Après :

<fr:dataTable value="#{factureView.lazyModel}"
              var="facture"
              paginator="true"
              rows="10"
              lazy="true"
              stripedRows="true">
    <p:column headerText="Numéro">
        <h:outputText value="#{facture.numero}"/>
    </p:column>
</fr:dataTable>

2. Créer des Composants Composites Métier

Créer un composant pour les badges de statut

Fichier : /WEB-INF/components/facture-statut-badge.xhtml

<?xml version="1.0" encoding="UTF-8"?>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="http://xmlns.jcp.org/jsf/html"
                xmlns:f="http://xmlns.jcp.org/jsf/core"
                xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
                xmlns:cc="http://xmlns.jcp.org/jsf/composite"
                xmlns:p="http://primefaces.org/ui">

    <cc:interface>
        <cc:attribute name="statut" required="true" type="java.lang.String"/>
        <cc:attribute name="enRetard" type="java.lang.Boolean" default="false"/>
    </cc:interface>

    <cc:implementation>
        <p:tag value="#{cc.attrs.statut}"
               severity="#{cc.attrs.statut == 'PAYEE' ? 'success' :
                          (cc.attrs.statut == 'ANNULEE' ? 'danger' :
                          (cc.attrs.enRetard ? 'danger' : 'warning'))}"
               icon="#{cc.attrs.statut == 'PAYEE' ? 'pi pi-check' :
                      (cc.attrs.statut == 'ANNULEE' ? 'pi pi-times' :
                      (cc.attrs.enRetard ? 'pi pi-exclamation-triangle' : 'pi pi-clock'))}"/>
    </cc:implementation>
</ui:composition>

Utilisation :

<p:column headerText="Statut">
    <btpx:facture-statut-badge statut="#{facture.statut}"
                               enRetard="#{factureView.isEnRetard(facture)}"/>
</p:column>

Créer un composant pour les montants

Fichier : /WEB-INF/components/montant-display.xhtml

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="http://xmlns.jcp.org/jsf/html"
                xmlns:f="http://xmlns.jcp.org/jsf/core"
                xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
                xmlns:cc="http://xmlns.jcp.org/jsf/composite">

    <cc:interface>
        <cc:attribute name="montant" required="true" type="java.lang.Double"/>
        <cc:attribute name="devise" default="Fcfa"/>
        <cc:attribute name="highlight" type="java.lang.Boolean" default="false"/>
        <cc:attribute name="highlightColor" default="red"/>
    </cc:interface>

    <cc:implementation>
        <span style="#{cc.attrs.highlight ? 'color: ' + cc.attrs.highlightColor + '; font-weight: bold;' : ''}">
            <h:outputText value="#{cc.attrs.montant}">
                <f:converter converterId="fcfaConverter"/>
            </h:outputText>
            <h:outputText value=" #{cc.attrs.devise}"/>
        </span>
    </cc:implementation>
</ui:composition>

Utilisation :

<p:column headerText="Reste à payer">
    <btpx:montant-display montant="#{factureView.getMontantRestant(facture)}"
                          highlight="#{factureView.getMontantRestant(facture) > 0}"/>
</p:column>

3. Créer un Composant de Filtre Réutilisable

Fichier : /WEB-INF/components/search-filter-panel.xhtml

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="http://xmlns.jcp.org/jsf/html"
                xmlns:f="http://xmlns.jcp.org/jsf/core"
                xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
                xmlns:cc="http://xmlns.jcp.org/jsf/composite"
                xmlns:p="http://primefaces.org/ui"
                xmlns:fr="http://primefaces.org/freya">

    <cc:interface>
        <cc:attribute name="bean" required="true"/>
        <cc:attribute name="tableId" required="true"/>
        <cc:facet name="filters" required="true"/>
    </cc:interface>

    <cc:implementation>
        <div class="card mb-3">
            <div class="flex align-items-center justify-content-between mb-3">
                <h3 class="m-0">
                    <i class="pi pi-filter mr-2"></i>
                    Filtres de recherche
                </h3>
                <div class="flex gap-2">
                    <fr:commandButton value="Rechercher"
                                      icon="pi pi-search"
                                      severity="primary"
                                      process="@this @parent"
                                      update="#{cc.attrs.tableId} messages"
                                      action="#{cc.attrs.bean.applyFilters}"/>

                    <fr:commandButton value="Réinitialiser"
                                      icon="pi pi-refresh"
                                      severity="secondary"
                                      outlined="true"
                                      process="@this"
                                      update="@parent #{cc.attrs.tableId}"
                                      action="#{cc.attrs.bean.resetFilters}"/>
                </div>
            </div>

            <cc:renderFacet name="filters"/>
        </div>
    </cc:implementation>
</ui:composition>

🎯 Gestion d'État & ViewScoped

1. Optimiser BaseListView

Problème actuel : Rechargement complet à chaque filtre

Solution : Ajouter un cache intelligent

@Getter
@Setter
public abstract class BaseListView<T, ID> implements Serializable {

    protected LazyDataModel<T> lazyModel;
    protected T selectedItem;
    protected boolean loading;

    // Cache pour éviter les rechargements inutiles
    private transient Map<String, Object> lastFilterParams;

    @PostConstruct
    public void init() {
        initializeLazyModel();
    }

    protected abstract void initializeLazyModel();

    public void applyFilters() {
        Map<String, Object> currentParams = buildFilterParams();

        // Vérifier si les filtres ont changé
        if (!Objects.equals(lastFilterParams, currentParams)) {
            lastFilterParams = new HashMap<>(currentParams);
            // Le LazyDataModel se rechargera automatiquement
        }
    }

    protected abstract Map<String, Object> buildFilterParams();

    public void resetFilters() {
        resetFilterFields();
        lastFilterParams = null;
        applyFilters();
    }

    protected abstract void resetFilterFields();
}

2. Utiliser @CacheResult pour les données statiques

Pour les listes de référence (statuts, types, etc.) :

@ApplicationScoped
public class ReferenceDataService {

    @CacheResult(cacheName = "statuts-facture")
    public List<SelectItem> getStatutsFacture() {
        return Arrays.asList(
            new SelectItem("BROUILLON", "Brouillon"),
            new SelectItem("EMISE", "Émise"),
            new SelectItem("PAYEE", "Payée"),
            // ...
        );
    }
}

Utilisation dans le bean :

@Named("factureView")
@ViewScoped
public class FactureView implements Serializable {

    @Inject
    ReferenceDataService refDataService;

    public List<SelectItem> getStatutsFacture() {
        return refDataService.getStatutsFacture(); // Mis en cache
    }
}

Validation & Messages

1. Validation côté client avec PrimeFaces

Activer la validation client dans application.properties :

primefaces.CLIENT_SIDE_VALIDATION=true
primefaces.CSV_ENABLED=true

Exemple de formulaire avec validation :

<h:form id="factureForm">
    <p:messages id="messages" showDetail="true" closable="true"/>

    <fr:fieldInput label="Numéro de facture"
                   value="#{factureView.entity.numero}"
                   required="true"
                   requiredMessage="Le numéro est obligatoire">
        <f:validateLength minimum="3" maximum="20"/>
    </fr:fieldInput>

    <fr:fieldCalendar label="Date d'émission"
                      value="#{factureView.entity.dateEmission}"
                      required="true"
                      showIcon="true">
        <f:validator validatorId="dateValidator"/>
    </fr:fieldCalendar>

    <fr:commandButton value="Enregistrer"
                      icon="pi pi-save"
                      action="#{factureView.save}"
                      update="messages"
                      process="@form"
                      validateClient="true"/>
</h:form>

2. Messages d'erreur personnalisés

Créer un validateur personnalisé :

@FacesValidator("dateValidator")
public class DateValidator implements Validator<LocalDate> {

    @Override
    public void validate(FacesContext context, UIComponent component, LocalDate value)
            throws ValidatorException {
        if (value != null && value.isBefore(LocalDate.now())) {
            FacesMessage msg = new FacesMessage(
                FacesMessage.SEVERITY_ERROR,
                "Date invalide",
                "La date ne peut pas être dans le passé"
            );
            throw new ValidatorException(msg);
        }
    }
}

3. Utiliser p:growl pour les notifications

Ajouter dans le template :

<p:growl id="growl"
         life="3000"
         sticky="false"
         showDetail="true"/>

Dans le bean :

public void save() {
    try {
        factureService.save(selectedItem);

        FacesContext.getCurrentInstance().addMessage(null,
            new FacesMessage(FacesMessage.SEVERITY_INFO,
                "Succès",
                "La facture a été enregistrée avec succès"));
    } catch (Exception e) {
        FacesContext.getCurrentInstance().addMessage(null,
            new FacesMessage(FacesMessage.SEVERITY_ERROR,
                "Erreur",
                "Impossible d'enregistrer la facture: " + e.getMessage()));
    }
}

📅 Plan d'Implémentation

Phase 1 : Optimisation des DataTables (Semaine 1)

  • Créer FactureLazyDataModel
  • Modifier FactureView pour utiliser LazyDataModel
  • Ajouter endpoints paginés dans le backend
  • Tester la pagination et le tri
  • Répliquer pour Devis, Clients, Chantiers

Phase 2 : Optimisation Ajax (Semaine 2)

  • Auditer tous les update="@form" et les remplacer
  • Ajouter process spécifiques sur tous les commandButton
  • Implémenter p:ajax pour les filtres en temps réel
  • Tester les performances

Phase 3 : Composants Réutilisables (Semaine 3)

  • Créer facture-statut-badge.xhtml
  • Créer montant-display.xhtml
  • Créer search-filter-panel.xhtml
  • Migrer vers fr:dataTable pour toutes les tables
  • Créer composants métier supplémentaires

Phase 4 : Validation & UX (Semaine 4)

  • Activer validation côté client
  • Créer validateurs personnalisés
  • Implémenter p:growl pour notifications
  • Ajouter confirmations pour actions critiques
  • Tests utilisateurs

Phase 5 : Cache & Performance (Semaine 5)

  • Implémenter cache pour données de référence
  • Optimiser BaseListView avec cache intelligent
  • Profiler et identifier les bottlenecks
  • Optimiser les requêtes backend

📊 Métriques de Succès

Avant Optimisation

  • ⏱️ Temps de chargement liste factures : ~2-3s (100+ factures)
  • 📦 Données transférées : Toutes les factures à chaque fois
  • 🔄 Re-rendering : Formulaire complet à chaque action
  • 💾 Mémoire : Toutes les données en mémoire

Après Optimisation (Objectifs)

  • ⏱️ Temps de chargement : <500ms (pagination)
  • 📦 Données transférées : 10-50 factures par page
  • 🔄 Re-rendering : Composants ciblés uniquement
  • 💾 Mémoire : Données paginées + cache intelligent
  • 🎯 Score Lighthouse : >90

🔗 Ressources

Documentation PrimeFaces

Exemples de Code

Articles & Tutoriels


🎓 Bonnes Pratiques Générales

DO

  • Utiliser LazyDataModel pour les grandes listes
  • Spécifier process et update de manière ciblée
  • Utiliser @ViewScoped pour les beans de vue
  • Créer des composants réutilisables
  • Valider côté client ET serveur
  • Utiliser le cache pour les données statiques
  • Tester les performances régulièrement

DON'T

  • Charger toutes les données en mémoire
  • Utiliser update="@all" ou update="@form" systématiquement
  • Oublier process sur les commandButton
  • Dupliquer le code de composants
  • Ignorer la validation côté client
  • Recharger les données de référence à chaque fois

🚀 Prochaines Étapes

  1. Commencer par Phase 1 : Lazy Loading pour Factures
  2. Mesurer les performances avant/après
  3. Itérer sur les autres modules
  4. Documenter les patterns réutilisables
  5. Former l'équipe aux nouvelles pratiques

Créé le : 2025-12-29 Auteur : Équipe BTPXpress Version : 1.0

2. Optimiser le Service Backend

Ajouter des endpoints paginés dans BtpXpressApiClient :

@Path("/api/factures")
public interface BtpXpressApiClient {

    @GET
    @Path("/lazy")
    @Produces(MediaType.APPLICATION_JSON)
    Response getFacturesLazy(
        @QueryParam("offset") int offset,
        @QueryParam("limit") int limit,
        @QueryParam("sortField") String sortField,
        @QueryParam("sortOrder") String sortOrder,
        @QueryParam("filter_numero") String filtreNumero,
        @QueryParam("filter_client") String filtreClient,
        @QueryParam("filter_statut") String filtreStatut
    );

    @GET
    @Path("/count")
    @Produces(MediaType.APPLICATION_JSON)
    int countFactures(
        @QueryParam("filter_numero") String filtreNumero,
        @QueryParam("filter_client") String filtreClient,
        @QueryParam("filter_statut") String filtreStatut
    );
}

Performance Ajax & Partial Rendering

1. Optimiser les Updates Ajax

Problème : Updates trop larges qui re-rendent des composants inutilement

Mauvaise pratique :

<p:commandButton value="Filtrer"
                 update="@form"
                 action="#{factureView.applyFilters}"/>

Bonne pratique :

<p:commandButton value="Filtrer"
                 update="facturesTable messages"
                 process="@this filtresPanel"
                 action="#{factureView.applyFilters}"/>

2. Utiliser process et update de manière ciblée