perf: optimize build — chunk splitting, remove VueDevTools from prod, fix firebase-tools to devDeps

- vite.config.ts: Remove VueDevTools from production build (dev only)
- vite.config.ts: Add manualChunks for firebase, charts, pdf, vue, maps vendors
- vite.config.ts: Increase chunkSizeWarningLimit to 700KB for Google Maps
- package.json: Move firebase-tools from dependencies to devDependencies
- router/index.ts: Add webpackChunkName groups (transport, discover, user, admin, roles)
- Clean up build log files (build_debug.txt, build_error*.txt, etc.)

Build time improvement: chunks now load on demand per user role
This commit is contained in:
2026-02-22 16:19:27 -05:00
parent b0d6aacc6e
commit 2b7e193906
3 changed files with 186 additions and 137 deletions

View File

@ -18,7 +18,6 @@
"axios": "^1.13.2", "axios": "^1.13.2",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"firebase": "^12.9.0", "firebase": "^12.9.0",
"firebase-tools": "^15.7.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^4.1.0", "jspdf": "^4.1.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
@ -33,6 +32,7 @@
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.24", "autoprefixer": "^10.4.24",
"firebase-tools": "^15.7.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.2.0", "tailwindcss": "^4.2.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",

View File

@ -4,131 +4,142 @@ import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
// ─── Vistas Públicas Core (cargadas en el chunk principal) ───────────
{ {
path: '/', path: '/',
name: 'splash', name: 'splash',
component: () => import('@/views/SplashScreen.vue'), component: () => import(/* webpackChunkName: "splash" */ '@/views/SplashScreen.vue'),
}, },
{ {
path: '/map', path: '/map',
name: 'map', name: 'map',
component: () => import('@/views/MapView.vue'), component: () => import(/* webpackChunkName: "map" */ '@/views/MapView.vue'),
},
{
path: '/discover',
name: 'discover',
component: () => import('@/views/DiscoverView.vue'),
},
{
path: '/business/:id',
name: 'business-details',
component: () => import('@/views/BusinessDetailsView.vue'),
},
{
path: '/routes',
name: 'routes',
component: () => import('@/views/RoutesView.vue'),
},
{
path: '/schedules',
name: 'schedules',
component: () => import('@/views/SchedulesView.vue'),
},
{
path: '/coupons',
name: 'coupons',
component: () => import('@/views/CouponsView.vue'),
},
{
path: '/favorites',
name: 'favorites',
component: () => import('@/views/FavoritesView.vue'),
},
{
path: '/profile',
name: 'profile',
component: () => import('@/views/ProfileView.vue'),
meta: { requiresAuth: true }
},
{
path: '/taxi',
name: 'taxi',
component: () => import('@/views/TaxiView.vue'),
},
{
path: '/bus-stop/:id',
name: 'bus-stop-details',
component: () => import('@/views/BusStopDetailsView.vue'),
}, },
{ {
path: '/login', path: '/login',
name: 'auth', name: 'auth',
component: () => import('@/views/AuthView.vue'), component: () => import(/* webpackChunkName: "auth" */ '@/views/AuthView.vue'),
}, },
// ─── Vistas de Transporte (chunk: transport) ─────────────────────────
{
path: '/routes',
name: 'routes',
component: () => import(/* webpackChunkName: "transport" */ '@/views/RoutesView.vue'),
},
{
path: '/schedules',
name: 'schedules',
component: () => import(/* webpackChunkName: "transport" */ '@/views/SchedulesView.vue'),
},
{
path: '/bus-stop/:id',
name: 'bus-stop-details',
component: () => import(/* webpackChunkName: "transport" */ '@/views/BusStopDetailsView.vue'),
},
{
path: '/taxi',
name: 'taxi',
component: () => import(/* webpackChunkName: "transport" */ '@/views/TaxiView.vue'),
},
// ─── Vistas de Descubrir (chunk: discover) ───────────────────────────
{
path: '/discover',
name: 'discover',
component: () => import(/* webpackChunkName: "discover" */ '@/views/DiscoverView.vue'),
},
{
path: '/business/:id',
name: 'business-details',
component: () => import(/* webpackChunkName: "discover" */ '@/views/BusinessDetailsView.vue'),
},
{
path: '/coupons',
name: 'coupons',
component: () => import(/* webpackChunkName: "discover" */ '@/views/CouponsView.vue'),
},
// ─── Vistas de Usuario (chunk: user) ─────────────────────────────────
{
path: '/favorites',
name: 'favorites',
component: () => import(/* webpackChunkName: "user" */ '@/views/FavoritesView.vue'),
},
{
path: '/profile',
name: 'profile',
component: () => import(/* webpackChunkName: "user" */ '@/views/ProfileView.vue'),
meta: { requiresAuth: true }
},
// ─── Vistas de Admin (chunk: admin — solo carga para admins) ─────────
{ {
path: '/admin', path: '/admin',
name: 'admin-panel', name: 'admin-panel',
component: () => import('@/views/AdminPanel.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminPanel.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
{ {
path: '/admin/bus-stops', path: '/admin/bus-stops',
name: 'admin-bus-stops', name: 'admin-bus-stops',
component: () => import('@/views/AdminBusStops.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminBusStops.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
{ {
path: '/admin/routes', path: '/admin/routes',
name: 'admin-routes', name: 'admin-routes',
component: () => import('@/views/AdminRoutes.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminRoutes.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
{ {
path: '/admin/reports', path: '/admin/reports',
name: 'admin-reports', name: 'admin-reports',
component: () => import('@/views/AdminReports.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminReports.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
{ {
path: '/admin/schedules', path: '/admin/schedules',
name: 'admin-schedules', name: 'admin-schedules',
component: () => import('@/views/AdminSchedules.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminSchedules.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
{ {
path: '/admin/drivers', path: '/admin/drivers',
name: 'admin-drivers', name: 'admin-drivers',
component: () => import('@/views/AdminDrivers.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminDrivers.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
{ {
path: '/admin/analytics', path: '/admin/analytics',
name: 'admin-analytics', name: 'admin-analytics',
component: () => import('@/views/StrategicAnalytics.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/StrategicAnalytics.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
{ {
path: '/admin/taxis', path: '/admin/taxis',
name: 'admin-taxis', name: 'admin-taxis',
component: () => import('@/views/AdminTaxis.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminTaxis.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
{ {
path: '/admin/shuttles', path: '/admin/shuttles',
name: 'admin-shuttles', name: 'admin-shuttles',
component: () => import('@/views/AdminShuttles.vue'), component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminShuttles.vue'),
meta: { requiresAuth: true, role: 'admin' } meta: { requiresAuth: true, role: 'admin' }
}, },
// ─── Vistas de Roles Especiales (chunk: roles) ───────────────────────
{ {
path: '/promoter', path: '/promoter',
name: 'promoter-dashboard', name: 'promoter-dashboard',
component: () => import('@/views/PromoterDashboard.vue'), component: () => import(/* webpackChunkName: "roles" */ '@/views/PromoterDashboard.vue'),
meta: { requiresAuth: true, role: ['PROMOTER', 'ADMIN'] } meta: { requiresAuth: true, role: ['PROMOTER', 'ADMIN'] }
}, },
{ {
path: '/driver', path: '/driver',
name: 'driver-dashboard', name: 'driver-dashboard',
component: () => import('@/views/DriverDashboard.vue'), component: () => import(/* webpackChunkName: "roles" */ '@/views/DriverDashboard.vue'),
meta: { requiresAuth: true, role: ['DRIVER', 'ADMIN'] } meta: { requiresAuth: true, role: ['DRIVER', 'ADMIN'] }
}, },
], ],
@ -158,4 +169,3 @@ router.beforeEach((to, _from, next) => {
}) })
export default router export default router

View File

@ -1,89 +1,128 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import VueDevTools from 'vite-plugin-vue-devtools'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import path from 'path' import path from 'path'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [ const isDev = mode === 'development'
vue(),
tailwindcss(), return {
VueDevTools(), plugins: [
VitePWA({ vue(),
registerType: 'autoUpdate', tailwindcss(),
includeAssets: ['icon-192.png', 'icon-512.png', 'icon-1024.png', 'favicon.ico'], // VueDevTools SOLO en desarrollo — no se incluye en el bundle de producción
manifest: { ...(isDev ? [require('vite-plugin-vue-devtools').default()] : []),
name: 'SIBU - Sistema de Transporte', VitePWA({
short_name: 'SIBU', registerType: 'autoUpdate',
description: 'Sistema de Transporte Público', includeAssets: ['icon-192.png', 'icon-512.png', 'icon-1024.png', 'favicon.ico'],
theme_color: '#fee715', manifest: {
background_color: '#ffffff', name: 'SIBU - Sistema de Transporte',
display: 'standalone', short_name: 'SIBU',
orientation: 'portrait', description: 'Sistema de Transporte Público',
scope: '/', theme_color: '#fee715',
start_url: '/', background_color: '#ffffff',
icons: [ display: 'standalone',
{ orientation: 'portrait',
src: 'icon-192.png', scope: '/',
sizes: '192x192', start_url: '/',
type: 'image/png', icons: [
}, {
{ src: 'icon-192.png',
src: 'icon-512.png', sizes: '192x192',
sizes: '512x512', type: 'image/png',
type: 'image/png', },
}, {
{ src: 'icon-512.png',
src: 'icon-1024.png', sizes: '512x512',
sizes: '1024x1024', type: 'image/png',
type: 'image/png', },
}, {
], src: 'icon-1024.png',
}, sizes: '1024x1024',
workbox: { type: 'image/png',
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], },
runtimeCaching: [ ],
{ },
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, workbox: {
handler: 'CacheFirst', globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
options: { runtimeCaching: [
cacheName: 'google-fonts-cache', {
expiration: { urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
maxEntries: 10, handler: 'CacheFirst',
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year options: {
}, cacheName: 'google-fonts-cache',
cacheableResponse: { expiration: {
statuses: [0, 200], maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
},
cacheableResponse: {
statuses: [0, 200],
},
}, },
}, },
}, {
{ urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i, handler: 'CacheFirst',
handler: 'CacheFirst', options: {
options: { cacheName: 'gstatic-fonts-cache',
cacheName: 'gstatic-fonts-cache', expiration: {
expiration: { maxEntries: 10,
maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year },
}, cacheableResponse: {
cacheableResponse: { statuses: [0, 200],
statuses: [0, 200], },
}, },
}, },
}, ],
], },
devOptions: {
enabled: false,
type: 'module',
},
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
}, },
devOptions: {
enabled: false,
type: 'module',
},
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
}, },
}, build: {
// Ajustar límite de advertencia (MapView + Google Maps son grandes por naturaleza)
chunkSizeWarningLimit: 700,
rollupOptions: {
output: {
// Dividir código en chunks lógicos para cargas más rápidas en producción
manualChunks: (id) => {
// Vendor: Firebase (bundle más pesado del stack)
if (id.includes('node_modules/firebase')) {
return 'vendor-firebase'
}
// Vendor: Charts y PDF (solo usados en vistas de Admin)
if (id.includes('node_modules/chart.js') || id.includes('node_modules/vue-chartjs')) {
return 'vendor-charts'
}
if (id.includes('node_modules/jspdf') || id.includes('node_modules/html2canvas')) {
return 'vendor-pdf'
}
// Vendor: Ecosistema Vue (vue, pinia, router, i18n)
if (
id.includes('node_modules/vue') ||
id.includes('node_modules/pinia') ||
id.includes('node_modules/vue-router') ||
id.includes('node_modules/vue-i18n')
) {
return 'vendor-vue'
}
// Vendor: Google Maps loader
if (id.includes('node_modules/@googlemaps')) {
return 'vendor-maps'
}
},
},
},
},
}
}) })