24 KiB
🚀 Optimisation BTPXpress - Meilleures Pratiques PrimeFaces
📋 Table des Matières
- Analyse du Projet Actuel
- Optimisations DataTable & Lazy Loading
- Performance Ajax & Partial Rendering
- Composants Réutilisables
- Gestion d'État & ViewScoped
- Validation & Messages
- Plan d'Implémentation
🔍 Analyse du Projet Actuel
Points Forts ✅
- ✅ Utilisation de
@ViewScopedpour 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
@formou@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
FactureViewpour 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
processspécifiques sur tous les commandButton - Implémenter
p:ajaxpour 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:dataTablepour 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
processetupdatede manière ciblée - ✅ Utiliser
@ViewScopedpour 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"ouupdate="@form"systématiquement - ❌ Oublier
processsur 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
- Commencer par Phase 1 : Lazy Loading pour Factures
- Mesurer les performances avant/après
- Itérer sur les autres modules
- Documenter les patterns réutilisables
- 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}"/>