perf(ops): lazy-load heavy libs & shell components — chunks route/shell bien plus légers

Batch 1 de l'audit d'optimisation. Les libs lourdes étaient statiquement importées
donc livrées même inutilisées → passées en import dynamique / defineAsyncComponent :
- MainLayout : ConversationPanel/PhoneModal/FlowEditor/NewTicket/Orchestrator/
  ServiceStatus/OutboxPanel → async (chunk shell 312 KB → 56 KB, chargé sur CHAQUE page)
- TaskGraphPage : TaskGantt (hy-vue-gantt) → async (2.6 MB → 20 KB ; le Gantt ne charge
  que dans la vue Gantt)
- NetworkPage : cytoscape → import() dans loadNetworkMap (484 KB → 52 KB)
- ReportAR/Revenu/Taxes : chart.js/auto → import() dans renderChart (async) (~150→~10 KB/page)

Total dist inchangé (~8 M) — le code est DÉFÉRÉ en chunks à la demande, pas supprimé.
Gain réel = ce que l'utilisateur télécharge pour VOIR une page. Build OK, leak-check propre,
shell + route rapport vérifiés (aucune erreur d'import).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-03 20:35:30 -04:00
parent 1af8c62142
commit 4a641ac802
6 changed files with 24 additions and 17 deletions

View File

@ -168,7 +168,7 @@
</template>
<script setup>
import { ref, computed, nextTick, watch } from 'vue'
import { ref, computed, nextTick, watch, defineAsyncComponent } from 'vue'
import { useQuasar } from 'quasar'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from 'src/stores/auth'
@ -180,16 +180,18 @@ import {
Gift, Settings, LogOut, PanelLeftOpen, PanelLeftClose, Mail,
CalendarRange, CalendarClock, Sparkles, MapPinned, Workflow, RefreshCw, ReceiptText, MessagesSquare, History, Award, Star, ClipboardCheck, HardHat,
} from 'lucide-vue-next'
import ConversationPanel from 'src/components/shared/ConversationPanel.vue'
import PhoneModal from 'src/components/customer/PhoneModal.vue'
// Composants lourds montés conditionnellement (v-if) chargés à la demande (defineAsyncComponent) au lieu d'alourdir
// le chunk MainLayout livré sur CHAQUE page. Aucun accès par ref sûr. NotificationBell reste synchrone (barre du haut, toujours visible).
const ConversationPanel = defineAsyncComponent(() => import('src/components/shared/ConversationPanel.vue'))
const PhoneModal = defineAsyncComponent(() => import('src/components/customer/PhoneModal.vue'))
import { useSoftphone } from 'src/composables/useSoftphone'
import NotificationBell from 'src/components/shared/NotificationBell.vue'
import { useConversations } from 'src/composables/useConversations'
import FlowEditorDialog from 'src/components/flow-editor/FlowEditorDialog.vue'
import NewTicketDialog from 'src/components/shared/NewTicketDialog.vue'
import OrchestratorDialog from 'src/components/shared/OrchestratorDialog.vue'
import ServiceStatusDialog from 'src/components/shared/ServiceStatusDialog.vue'
import OutboxPanel from 'src/components/shared/OutboxPanel.vue'
const FlowEditorDialog = defineAsyncComponent(() => import('src/components/flow-editor/FlowEditorDialog.vue'))
const NewTicketDialog = defineAsyncComponent(() => import('src/components/shared/NewTicketDialog.vue'))
const OrchestratorDialog = defineAsyncComponent(() => import('src/components/shared/OrchestratorDialog.vue'))
const ServiceStatusDialog = defineAsyncComponent(() => import('src/components/shared/ServiceStatusDialog.vue'))
const OutboxPanel = defineAsyncComponent(() => import('src/components/shared/OutboxPanel.vue'))
const icons = { LayoutDashboard, Users, Truck, Ticket, UsersRound, BarChart3, Gift, Settings, LogOut, PanelLeftOpen, PanelLeftClose, Mail, CalendarRange, CalendarClock, Sparkles, MapPinned, Workflow, RefreshCw, ReceiptText, MessagesSquare, History, Award, Star, ClipboardCheck, HardHat }

View File

@ -696,7 +696,6 @@
<script setup>
import { ref, computed, onMounted, watch, nextTick, reactive } from 'vue'
import cytoscape from 'cytoscape'
import { useRouter } from 'vue-router'
import { listDocs, updateDoc, createDoc } from 'src/api/erp'
import { Notify } from 'quasar'
@ -1172,6 +1171,7 @@ const mapSelectedSite = ref(null)
const mapSelectedSiteId = ref(null)
const cyContainer = ref(null)
let cyInstance = null
let cytoscape = null // chargé à la demande (lib ~350 KB) seulement quand on ouvre l'onglet Carte
const mapSelectedSiteLinks = computed(() => {
if (!mapData.value || !mapSelectedSiteId.value) return []
@ -1183,6 +1183,7 @@ async function loadNetworkMap () {
try {
const resp = await fetch(`${hubUrl}/network/map`)
mapData.value = await resp.json()
if (!cytoscape) cytoscape = (await import('cytoscape')).default // import dynamique : sort la lib du chunk NetworkPage
await nextTick()
renderCytoscape()
} catch (e) {

View File

@ -30,7 +30,7 @@ import { fetchARReport } from 'src/api/reports'
import { formatMoney } from 'src/composables/useFormatters'
import { exportCsv } from 'src/composables/useCsvExport'
import ReportScaffold from 'src/components/shared/ReportScaffold.vue'
import Chart from 'chart.js/auto'
let Chart = null // chart.js ~90 KB chargé à la demande au 1er rendu de graphique (hors du chunk de la page)
const asOfDate = ref(new Date().toISOString().slice(0, 10))
const loading = ref(false)
@ -83,9 +83,10 @@ async function loadReport () {
}
}
function renderChart () {
async function renderChart () {
if (chartInstance) chartInstance.destroy()
if (!chartCanvas.value || !summary.value) return
if (!Chart) Chart = (await import('chart.js/auto')).default
const s = summary.value
chartInstance = new Chart(chartCanvas.value, {
type: 'doughnut',

View File

@ -104,7 +104,7 @@ import { fetchRevenueReport } from 'src/api/reports'
import { formatMoney } from 'src/composables/useFormatters'
import StatCard from 'src/components/shared/StatCard.vue'
import DataTable from 'src/components/shared/DataTable.vue'
import Chart from 'chart.js/auto'
let Chart = null // chart.js ~90 KB chargé à la demande au 1er rendu de graphique (hors du chunk de la page)
const now = new Date()
const startDate = ref(new Date(now.getFullYear(), 0, 1).toISOString().slice(0, 10))
@ -197,9 +197,10 @@ const CHART_COLORS = [
'#a855f7', '#3b82f6', '#d946ef', '#0ea5e9', '#f43f5e',
]
function renderChart () {
async function renderChart () {
if (chartInstance) chartInstance.destroy()
if (!chartCanvas.value || !chartAccounts.value.length) return
if (!Chart) Chart = (await import('chart.js/auto')).default
const months = data.value.months || []
const labels = months.map(m => {

View File

@ -73,7 +73,7 @@ import { fetchTaxReport } from 'src/api/reports'
import { formatMoney } from 'src/composables/useFormatters'
import { exportCsv } from 'src/composables/useCsvExport'
import ReportScaffold from 'src/components/shared/ReportScaffold.vue'
import Chart from 'chart.js/auto'
let Chart = null // chart.js ~90 KB chargé à la demande au 1er rendu de graphique (hors du chunk de la page)
const now = new Date()
const startDate = ref(new Date(now.getFullYear(), 0, 1).toISOString().slice(0, 10))
@ -136,9 +136,10 @@ async function loadReport () {
}
}
function renderChart () {
async function renderChart () {
if (chartInstance) chartInstance.destroy()
if (!chartCanvas.value || !periods.value.length) return
if (!Chart) Chart = (await import('chart.js/auto')).default
const labels = periods.value.map(p => p.label)
chartInstance = new Chart(chartCanvas.value, {
type: 'bar',

View File

@ -73,12 +73,13 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted, defineAsyncComponent } from 'vue'
import { useQuasar } from 'quasar'
import { listFlowTemplates, getFlowTemplate, updateFlowTemplate } from 'src/api/flow-templates'
import { HUB_URL } from 'src/config/hub'
import TaskDependencyGraph from 'src/components/flow-editor/TaskDependencyGraph.vue'
import TaskGantt from 'src/components/flow-editor/TaskGantt.vue'
// Vue Gantt (hy-vue-gantt, lourd) rendu seulement dans la vue « gantt » chargé à la demande.
const TaskGantt = defineAsyncComponent(() => import('src/components/flow-editor/TaskGantt.vue'))
const $q = useQuasar()
const view = ref('dag')