perf(cfdi): optimize page performance

Database optimizations:
- Add indexes on fecha_emision, tipo, estado, rfc_emisor, rfc_receptor
- Add trigram indexes for fast ILIKE searches on nombre fields
- Combine COUNT with main query using window function (1 query instead of 2)

Frontend optimizations:
- Add 300ms debounce to autocomplete searches
- Add staleTime (30s) and gcTime (5min) to useCfdis hook
- Reduce unnecessary API calls on every keystroke

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-02-17 07:15:33 +00:00
parent 0e49c0922d
commit 08a7312761
4 changed files with 64 additions and 50 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useRef, useCallback } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
import { useDebounce } from '@/lib/hooks/use-debounce';
import { Header } from '@/components/layouts/header';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -240,8 +241,39 @@ export default function CfdiPage() {
const [openFilter, setOpenFilter] = useState<'fecha' | 'emisor' | 'receptor' | null>(null);
const [emisorSuggestions, setEmisorSuggestions] = useState<EmisorReceptor[]>([]);
const [receptorSuggestions, setReceptorSuggestions] = useState<EmisorReceptor[]>([]);
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
const [loadingEmisor, setLoadingEmisor] = useState(false);
const [loadingReceptor, setLoadingReceptor] = useState(false);
const [showForm, setShowForm] = useState(false);
// Debounced values for autocomplete
const debouncedEmisor = useDebounce(columnFilters.emisor, 300);
const debouncedReceptor = useDebounce(columnFilters.receptor, 300);
// Fetch emisor suggestions when debounced value changes
useEffect(() => {
if (debouncedEmisor.length < 2) {
setEmisorSuggestions([]);
return;
}
setLoadingEmisor(true);
searchEmisores(debouncedEmisor)
.then(setEmisorSuggestions)
.catch(() => setEmisorSuggestions([]))
.finally(() => setLoadingEmisor(false));
}, [debouncedEmisor]);
// Fetch receptor suggestions when debounced value changes
useEffect(() => {
if (debouncedReceptor.length < 2) {
setReceptorSuggestions([]);
return;
}
setLoadingReceptor(true);
searchReceptores(debouncedReceptor)
.then(setReceptorSuggestions)
.catch(() => setReceptorSuggestions([]))
.finally(() => setLoadingReceptor(false));
}, [debouncedReceptor]);
const [showBulkForm, setShowBulkForm] = useState(false);
const [formData, setFormData] = useState<CreateCfdiData>(initialFormData);
const [bulkData, setBulkData] = useState('');
@@ -291,42 +323,6 @@ export default function CfdiPage() {
setFilters({ ...filters, search: searchTerm, page: 1 });
};
// Debounced search for emisor suggestions
const handleEmisorSearch = useCallback(async (value: string) => {
setColumnFilters(prev => ({ ...prev, emisor: value }));
if (value.length < 2) {
setEmisorSuggestions([]);
return;
}
setLoadingSuggestions(true);
try {
const results = await searchEmisores(value);
setEmisorSuggestions(results);
} catch {
setEmisorSuggestions([]);
} finally {
setLoadingSuggestions(false);
}
}, []);
// Debounced search for receptor suggestions
const handleReceptorSearch = useCallback(async (value: string) => {
setColumnFilters(prev => ({ ...prev, receptor: value }));
if (value.length < 2) {
setReceptorSuggestions([]);
return;
}
setLoadingSuggestions(true);
try {
const results = await searchReceptores(value);
setReceptorSuggestions(results);
} catch {
setReceptorSuggestions([]);
} finally {
setLoadingSuggestions(false);
}
}, []);
const selectEmisor = (emisor: EmisorReceptor) => {
setColumnFilters(prev => ({ ...prev, emisor: emisor.nombre }));
setEmisorSuggestions([]);
@@ -1285,7 +1281,7 @@ export default function CfdiPage() {
placeholder="Buscar emisor..."
className="h-8 text-sm"
value={columnFilters.emisor}
onChange={(e) => handleEmisorSearch(e.target.value)}
onChange={(e) => setColumnFilters(prev => ({ ...prev, emisor: e.target.value }))}
onKeyDown={(e) => e.key === 'Enter' && applyEmisorFilter()}
/>
{emisorSuggestions.length > 0 && (
@@ -1303,7 +1299,7 @@ export default function CfdiPage() {
))}
</div>
)}
{loadingSuggestions && columnFilters.emisor.length >= 2 && (
{loadingEmisor && columnFilters.emisor.length >= 2 && emisorSuggestions.length === 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-900 border rounded-md shadow-lg p-2 text-center text-sm text-muted-foreground">
Buscando...
</div>
@@ -1342,7 +1338,7 @@ export default function CfdiPage() {
placeholder="Buscar receptor..."
className="h-8 text-sm"
value={columnFilters.receptor}
onChange={(e) => handleReceptorSearch(e.target.value)}
onChange={(e) => setColumnFilters(prev => ({ ...prev, receptor: e.target.value }))}
onKeyDown={(e) => e.key === 'Enter' && applyReceptorFilter()}
/>
{receptorSuggestions.length > 0 && (
@@ -1360,7 +1356,7 @@ export default function CfdiPage() {
))}
</div>
)}
{loadingSuggestions && columnFilters.receptor.length >= 2 && (
{loadingReceptor && columnFilters.receptor.length >= 2 && receptorSuggestions.length === 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-900 border rounded-md shadow-lg p-2 text-center text-sm text-muted-foreground">
Buscando...
</div>

View File

@@ -7,6 +7,8 @@ export function useCfdis(filters: CfdiFilters) {
return useQuery({
queryKey: ['cfdis', filters],
queryFn: () => cfdiApi.getCfdis(filters),
staleTime: 30 * 1000, // 30 segundos
gcTime: 5 * 60 * 1000, // 5 minutos en cache
});
}

View File

@@ -0,0 +1,17 @@
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}