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