Lazy loading além de imagens: componentes, rotas e dados sob demanda
1. Fundamentos do Lazy Loading no Contexto Moderno
1.1. O princípio da carga sob demanda vs. carga total
O lazy loading tradicional de imagens já é uma prática consolidada, mas o verdadeiro ganho de performance em aplicações modernas está em estender esse conceito para componentes, rotas e dados. A diferença fundamental está no impacto sobre o bundle size e o tempo de interação.
Enquanto imagens lazy representam economia de banda, o lazy loading de componentes elimina código JavaScript da carga inicial. Considere o cenário típico:
// Carga total (sem lazy)
import ChartLibrary from 'heavy-chart-library'
// Bundle final: 2.5 MB
// Carga sob demanda (com lazy)
const ChartLibrary = React.lazy(() => import('heavy-chart-library'))
// Bundle inicial: 1.2 MB (economia de 52%)
1.2. Diferença entre lazy loading de imagens e recursos dinâmicos
Imagens são recursos estáticos que bloqueiam renderização. Componentes, rotas e dados são recursos dinâmicos que bloqueiam interatividade. A métrica crítica aqui é o First Input Delay (FID) e o Time to Interactive (TTI).
1.3. Métricas de desempenho impactadas
- LCP (Largest Contentful Paint): Reduzido ao não carregar componentes pesados no paint inicial
- FID (First Input Delay): Melhorado pois o thread principal fica livre para responder a interações
- TBT (Total Blocking Time): Reduzido drasticamente com code splitting bem feito
2. Lazy Loading de Componentes e Bibliotecas
2.1. Code splitting com import() dinâmico
A técnica central é o import() dinâmico, que cria chunks separados no bundle:
// Sem lazy loading
import { Editor } from 'rich-text-editor'
// Com lazy loading condicional
const loadEditor = () => import('rich-text-editor')
// Uso com Intersection Observer
const editorRef = useRef(null)
const [EditorComponent, setEditorComponent] = useState(null)
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadEditor().then((module) => {
setEditorComponent(() => module.Editor)
})
observer.disconnect()
}
})
observer.observe(editorRef.current)
}, [])
2.2. Técnicas de carregamento condicional
Para bibliotecas pesadas como gráficos ou mapas, a abordagem "carregar apenas na interação" é ideal:
const MapComponent = () => {
const [loadMap, setLoadMap] = useState(false)
return (
<div onClick={() => setLoadMap(true)}>
{loadMap ? (
<LazyMapComponent />
) : (
<div className="map-placeholder">
Clique para carregar o mapa
</div>
)}
</div>
)
}
3. Lazy Loading de Rotas (Route-based Splitting)
3.1. Implementação com React.lazy + Suspense
O roteamento é o caso de uso mais comum para lazy loading em aplicações SPA:
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Reports = lazy(() => import('./pages/Reports'))
const AdminPanel = lazy(() => import('./pages/Admin'))
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
3.2. Pré-carregamento inteligente de rotas
Para melhorar a experiência, podemos pré-carregar chunks ao detectar intenção:
// Prerregamento ao hover em links
const prefetchRoute = (routePath) => {
const routeMap = {
'/dashboard': () => import('./pages/Dashboard'),
'/reports': () => import('./pages/Reports')
}
if (routeMap[routePath]) {
routeMap[routePath]()
}
}
// Uso no componente Link
<Link
to="/reports"
onMouseEnter={() => prefetchRoute('/reports')}
>
Relatórios
</Link>
3.3. Tratamento de fallback em rotas lentas
const ErrorFallback = ({ error, resetErrorBoundary }) => (
<div role="alert">
<p>Falha ao carregar esta página:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Tentar novamente</button>
</div>
)
// Uso com ErrorBoundary
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<SkeletonLoader />}>
<LazyRoute />
</Suspense>
</ErrorBoundary>
4. Lazy Loading de Dados (Data Fetching Sob Demanda)
4.1. Paginação infinita com virtual scrolling
const InfiniteList = () => {
const [items, setItems] = useState([])
const [page, setPage] = useState(1)
const loaderRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
fetch(`/api/items?page=${page}`)
.then(res => res.json())
.then(newItems => {
setItems(prev => [...prev, ...newItems])
setPage(p => p + 1)
})
}
})
observer.observe(loaderRef.current)
}, [page])
return (
<div>
{items.map(item => <ItemCard key={item.id} data={item} />)}
<div ref={loaderRef}>Carregando...</div>
</div>
)
}
4.2. Fetch on interaction
const ExpandableSection = ({ sectionId }) => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const handleExpand = async () => {
setLoading(true)
const response = await fetch(`/api/sections/${sectionId}`)
const result = await response.json()
setData(result)
setLoading(false)
}
return (
<div>
<button onClick={handleExpand} disabled={loading}>
{loading ? 'Carregando...' : 'Expandir seção'}
</button>
{data && <SectionContent content={data} />}
</div>
)
}
5. Integração com Gerenciamento de Estado e Cache
5.1. Gerenciamento de estados em componentes lazy
const LazyComponent = () => {
const { data, error, isLoading } = useQuery({
queryKey: ['expensive-data'],
queryFn: () => fetch('/api/expensive-endpoint').then(r => r.json()),
enabled: true // controla quando a query é executada
})
if (isLoading) return <Skeleton />
if (error) return <ErrorMessage error={error} />
return <DataViewer data={data} />
}
5.2. Cache com TanStack Query
const { data } = useQuery({
queryKey: ['dashboard', userId],
queryFn: () => fetchDashboardData(userId),
staleTime: 5 * 60 * 1000, // 5 minutos de cache
cacheTime: 30 * 60 * 1000, // 30 minutos até ser removido
})
6. Performance e Experiência do Usuário
6.1. Priorização de recursos
// Recursos críticos (carregamento imediato)
import { Header, Navigation } from './critical'
// Recursos não-críticos (lazy)
const Footer = lazy(() => import('./Footer'))
const ChatWidget = lazy(() => import('./ChatWidget'))
6.2. Skeleton screens específicos
const SkeletonLoader = () => (
<div className="skeleton" aria-label="Carregando conteúdo">
<div className="skeleton-line" style={{ width: '80%' }} />
<div className="skeleton-line" style={{ width: '60%' }} />
<div className="skeleton-block" style={{ height: '200px' }} />
</div>
)
7. Considerações de Acessibilidade e SEO
7.1. Garantindo indexação
Para conteúdo lazy que precisa ser indexado por crawlers, utilize prerendering ou SSR híbrido:
// No servidor (Next.js)
export async function getStaticProps() {
const data = await fetchDataForSEO()
return { props: { data } }
}
7.2. Acessibilidade em carregamentos assíncronos
const AsyncContent = () => {
const [content, setContent] = useState(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
fetchContent().then(data => {
setContent(data)
setIsLoading(false)
// Anunciar para leitores de tela
const announcement = document.createElement('div')
announcement.setAttribute('role', 'status')
announcement.setAttribute('aria-live', 'polite')
announcement.textContent = 'Conteúdo carregado'
document.body.appendChild(announcement)
})
}, [])
return (
<div aria-busy={isLoading}>
{isLoading ? <Skeleton /> : content}
</div>
)
}
8. Padrões Avançados e Casos de Uso Específicos
8.1. Lazy loading em modais e tabs
const ModalWithLazyContent = ({ isOpen }) => {
return (
<dialog open={isOpen}>
{isOpen && (
<Suspense fallback={<ModalSkeleton />}>
<HeavyModalContent />
</Suspense>
)}
</dialog>
)
}
8.2. Listas virtualizadas com dados lazy
import { FixedSizeList } from 'react-window'
const VirtualizedList = ({ items }) => {
const Row = ({ index, style }) => {
const item = items[index]
return (
<div style={style}>
{item ? <ItemComponent data={item} /> : <Placeholder />}
</div>
)
}
return (
<FixedSizeList
height={600}
itemCount={10000}
itemSize={50}
>
{Row}
</FixedSizeList>
)
}
8.3. Estratégia híbrida com prefetching preditivo
const PredictivePrefetcher = () => {
useEffect(() => {
const prefetchOnIdle = () => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// Pré-carregar componentes prováveis
import('./LikelyNextComponent')
import('./LikelyNextRoute')
})
}
}
window.addEventListener('load', prefetchOnIdle)
return () => window.removeEventListener('load', prefetchOnIdle)
}, [])
}
Referências
- React.lazy e Suspense - Documentação Oficial — Guia completo sobre code splitting com React.lazy e Suspense para componentes e rotas.
- Code Splitting com Webpack — Documentação oficial do Webpack sobre técnicas de code splitting e lazy loading.
- Intersection Observer API - MDN — Referência técnica para implementação de lazy loading baseado em visibilidade.
- TanStack Query - Data Fetching Sob Demanda — Biblioteca para gerenciamento de cache e fetching de dados com suporte a lazy loading.
- React Virtualized - Listas Grandes — Biblioteca para renderização virtualizada de listas com lazy loading de dados.
- Next.js Dynamic Imports — Guia oficial do Next.js para lazy loading de componentes e bibliotecas em aplicações SSR/SSG.
- Web Vitals - Métricas de Performance — Artigo do Google sobre métricas de performance impactadas por lazy loading (LCP, FID, CLS).
- Acessibilidade em Aplicações SPA — Padrões ARIA para garantir acessibilidade em carregamentos assíncronos e lazy loading.
- SWR - React Hooks para Data Fetching — Biblioteca leve para fetching de dados com cache e revalidação automática.
- Intersection Observer para Lazy Loading — Tutorial do Google Chrome Developers sobre uso prático do Intersection Observer.