Los martes resolvemos problemas reales. Hoy analizamos los cuellos de botella de performance más comunes en aplicaciones React y las técnicas sistemáticas para identificarlos antes de que arruinen la experiencia de usuario.
Problema #1: Re-renders Innecesarios - El Drain de Performance
Síntomas:
- Aplicación lenta en interacciones simples
- CPU alta en DevTools durante typing
- Animaciones que se sienten laggy
Código Problemático:
// ❌ Parent que causa re-renders en cadena
function Dashboard({ user }) {
const [searchTerm, setSearchTerm] = useState('');
// Object creado en cada render
const userPrefs = {
theme: user.theme,
language: user.language
};
return (
<div>
<SearchBar
onSearch={setSearchTerm}
userPrefs={userPrefs} // Nueva referencia siempre
/>
<UserStats user={user} />
<ProjectList searchTerm={searchTerm} />
</div>
);
}
// Child que re-renderiza innecesariamente
function UserStats({ user }) {
console.log('UserStats rendered'); // Se ejecuta siempre
return (
<div>
<h3>{user.name}</h3>
<p>{user.projectCount} proyectos</p>
</div>
);
}
Solución Sistemática:
// ✅ Memoization estratégica
function Dashboard({ user }) {
const [searchTerm, setSearchTerm] = useState('');
// Memoizar objects complejos
const userPrefs = useMemo(() => ({
theme: user.theme,
language: user.language
}), [user.theme, user.language]);
// Memoizar callbacks
const handleSearch = useCallback((term) => {
setSearchTerm(term);
}, []);
return (
<div>
<SearchBar
onSearch={handleSearch}
userPrefs={userPrefs}
/>
<UserStats user={user} />
<ProjectList searchTerm={searchTerm} />
</div>
);
}
// Memoizar component que depende solo de props específicas
const UserStats = memo(function UserStats({ user }) {
console.log('UserStats rendered');
return (
<div>
<h3>{user.name}</h3>
<p>{user.projectCount} proyectos</p>
</div>
);
}, (prevProps, nextProps) => {
// Custom comparison para optimización específica
return prevProps.user.name === nextProps.user.name &&
prevProps.user.projectCount === nextProps.user.projectCount;
});
Debugging con React DevTools:
// Componente para detectar re-renders en desarrollo
function RenderCounter({ name }) {
const renderCount = useRef(0);
renderCount.current++;
if (process.env.NODE_ENV === 'development') {
console.log(`${name} rendered ${renderCount.current} times`);
}
return null;
}
// Uso en components sospechosos
function MyComponent() {
return (
<div>
<RenderCounter name="MyComponent" />
{/* resto del component */}
</div>
);
}
Problema #2: Bundle Size Descontrolado
Síntomas:
- First Load muy lento
- Lighthouse Performance Score bajo
- High bounce rate en analytics
Análisis de Bundle:
# Analizar bundle size
npm install --save-dev webpack-bundle-analyzer
npm run build
npx webpack-bundle-analyzer build/static/js/*.js
Code Splitting Inteligente:
// ❌ Import estático de componente pesado
import HeavyChart from './components/HeavyChart';
import ExpensiveModal from './components/ExpensiveModal';
// ✅ Lazy loading con Suspense
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const ExpensiveModal = lazy(() => import('./components/ExpensiveModal'));
function Dashboard() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
{showModal && (
<Suspense fallback={<ModalSkeleton />}>
<ExpensiveModal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
);
}
Bundle Optimization:
// Utility para cargar dependencies solo cuando necesario
function useConditionalImport(shouldLoad) {
const [module, setModule] = useState(null);
useEffect(() => {
if (shouldLoad && !module) {
import('heavy-library').then(mod => {
setModule(mod.default);
});
}
}, [shouldLoad, module]);
return module;
}
// Uso
function DataVisualization({ showAdvanced }) {
const D3 = useConditionalImport(showAdvanced);
if (showAdvanced && D3) {
return <D3Chart />;
}
return <SimpleChart />;
}
Problema #3: State Updates Masivos y Ineficientes
Problema:
// ❌ Updates frecuentes que bloquean UI
function RealTimeData() {
const [data, setData] = useState([]);
useEffect(() => {
const socket = io();
socket.on('update', (newItem) => {
// Update por cada item - causa muchos re-renders
setData(prev => [...prev, newItem]);
});
// Updates cada 100ms
const interval = setInterval(() => {
fetchLatestData().then(setData);
}, 100);
return () => {
socket.disconnect();
clearInterval(interval);
};
}, []);
return (
<div>
{data.map(item => <ExpensiveItem key={item.id} item={item} />)}
</div>
);
}
Optimización de State:
// ✅ Batching y throttling inteligente
function RealTimeData() {
const [data, setData] = useState([]);
const pendingUpdates = useRef([]);
// Batch updates para evitar re-renders frecuentes
const flushUpdates = useCallback(
throttle(() => {
if (pendingUpdates.current.length > 0) {
setData(prev => [...prev, ...pendingUpdates.current]);
pendingUpdates.current = [];
}
}, 200),
[]
);
useEffect(() => {
const socket = io();
socket.on('update', (newItem) => {
// Accumular updates en lugar de aplicar inmediatamente
pendingUpdates.current.push(newItem);
flushUpdates();
});
return () => {
socket.disconnect();
flushUpdates.cancel();
};
}, [flushUpdates]);
// Virtualización para listas largas
return (
<FixedSizeList
height={600}
itemCount={data.length}
itemSize={100}
>
{({ index, style }) => (
<div style={style}>
<ExpensiveItem item={data[index]} />
</div>
)}
</FixedSizeList>
);
}
Problema #4: Images y Assets Sin Optimizar
Performance Killer:
// ❌ Images sin optimización
function ProductGallery({ products }) {
return (
<div>
{products.map(product => (
<img
key={product.id}
src={product.imageUrl} // Full size siempre
alt={product.name}
/>
))}
</div>
);
}
Image Optimization:
// ✅ Responsive images con lazy loading
function OptimizedImage({ src, alt, className }) {
const [isLoading, setIsLoading] = useState(true);
const [imageSrc, setImageSrc] = useState(null);
const imgRef = useRef();
useEffect(() => {
const img = imgRef.current;
if (!img) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
// Determinar tamaño óptimo basado en viewport
const width = img.offsetWidth;
const optimizedSrc = `${src}?w=${width}&q=80`;
setImageSrc(optimizedSrc);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
observer.observe(img);
return () => observer.disconnect();
}, [src]);
return (
<div className={className}>
<img
ref={imgRef}
src={imageSrc}
alt={alt}
onLoad={() => setIsLoading(false)}
style={{ opacity: isLoading ? 0 : 1 }}
/>
{isLoading && <ImageSkeleton />}
</div>
);
}
// Component para gallery optimizada
function ProductGallery({ products }) {
return (
<div className="gallery">
{products.map(product => (
<OptimizedImage
key={product.id}
src={product.imageUrl}
alt={product.name}
className="product-image"
/>
))}
</div>
);
}
Problema #5: useEffect Dependencies Hell
Dependency Issues:
// ❌ useEffect que se ejecuta demasiado
function UserProfile({ userId, preferences }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
// Se ejecuta en cada render porque 'preferences' es nuevo object
fetchUserProfile(userId, preferences).then(setProfile);
}, [userId, preferences]);
// Otro effect que causa loops infinitos
useEffect(() => {
if (profile) {
// updateProfile creates new reference
setProfile(updateProfile(profile));
}
}, [profile]); // Infinite loop!
return <div>{/* UI */}</div>;
}
Dependencies Optimizadas:
// ✅ Dependencies controladas y estables
function UserProfile({ userId, preferences }) {
const [profile, setProfile] = useState(null);
// Extraer valores primitivos de objects complejos
const { theme, language } = preferences;
useEffect(() => {
fetchUserProfile(userId, { theme, language }).then(setProfile);
}, [userId, theme, language]); // Dependencies primitivas estables
// Evitar loops con functional updates
useEffect(() => {
if (profile && profile.needsUpdate) {
setProfile(prev => ({
...prev,
...updateProfile(prev),
needsUpdate: false
}));
}
}, [profile?.needsUpdate]); // Specific dependency
// Alternative: usar useCallback para stable references
const handleProfileUpdate = useCallback((updates) => {
setProfile(prev => ({ ...prev, ...updates }));
}, []);
return <div>{/* UI */}</div>;
}
Herramientas de Performance Debugging
React DevTools Profiler:
// Wrapper para profiling en desarrollo
function ProfiledComponent({ children, name }) {
if (process.env.NODE_ENV !== 'development') {
return children;
}
return (
<Profiler
id={name}
onRender={(id, phase, actualDuration) => {
if (actualDuration > 16) { // > 1 frame
console.warn(`${id} took ${actualDuration}ms in ${phase}`);
}
}}
>
{children}
</Profiler>
);
}
Performance Monitoring:
// Hook para monitorear performance metrics
function usePerformanceMonitor() {
useEffect(() => {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name === 'measure') {
console.log(`${entry.name}: ${entry.duration}ms`);
}
});
});
observer.observe({ entryTypes: ['measure'] });
return () => observer.disconnect();
}, []);
}
// Uso en componente crítico
function CriticalComponent() {
usePerformanceMonitor();
useEffect(() => {
performance.mark('critical-start');
// Operación pesada
heavyComputation();
performance.mark('critical-end');
performance.measure('critical-operation', 'critical-start', 'critical-end');
}, []);
return <div>{/* Component */}</div>;
}
Performance Checklist
Pre-optimization:
- React DevTools Profiler instalado
- Bundle analyzer configurado
- Performance budget definido
- Core Web Vitals baseline establecido
Components:
- memo() en components que reciben props complejas
- useMemo() para calculations costosos
- useCallback() para event handlers
- Dependencies de useEffect minimizadas
Data:
- State normalizado (no nested objects profundos)
- Updates batcheados cuando sea posible
- Lazy loading para data no crítica
- Virtualización en listas largas
Assets:
- Images optimizadas y lazy loaded
- Code splitting implementado
- Dependencies analizadas y tree-shaken
- Service worker para caching
Pro Tips de Performance
1. Measure First:
# Lighthouse CI en cada deploy
npm install -g @lhci/cli
lhci autorun --collect.numberOfRuns=3
2. Performance Budget:
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 250000,
hints: "error"
}
}
3. Runtime Monitoring:
// Monitor de Core Web Vitals
function reportWebVitals(metric) {
if (process.env.NODE_ENV === 'production') {
// Enviar a analytics
gtag('event', metric.name, {
value: Math.round(metric.value),
event_label: metric.label
});
}
}
¿Cuál de estos problemas de performance les ha dado más dolores de cabeza? ¿Tienen alguna técnica de debugging específica para React que no mencioné?
La performance en React no se trata de optimizar todo, sino de optimizar lo correcto. El 80% de los problemas suelen estar en el 20% de componentes más críticos.
#TroubleshootingTuesday react performance webdev optimization javascript
