Lazy loading e code splitting com tipos

1. Fundamentos do Lazy Loading com TypeScript

A importação estática (import) é a forma tradicional de carregar módulos em TypeScript, onde todos os módulos são resolvidos em tempo de compilação. Já a importação dinâmica (import()) permite carregar módulos sob demanda, em tempo de execução. O TypeScript infere o tipo do retorno de import() como Promise<typeof module>, o que significa que você obtém um objeto contendo todas as exportações do módulo.

// Importação estática
import { heavyModule } from './heavyModule';

// Importação dinâmica
const loadHeavyModule = async () => {
  const module = await import('./heavyModule');
  // module é do tipo typeof import('./heavyModule')
  module.heavyFunction();
};

O tipo Promise<typeof module> é fundamental para garantir que o código consuma corretamente as exportações do módulo carregado dinamicamente, mantendo a segurança de tipos.

2. Code Splitting Automático com Vite e Webpack

Bundlers como Vite e Webpack detectam automaticamente imports dinâmicos e criam chunks separados para cada um. No Vite, a configuração padrão já otimiza o code splitting. No Webpack, é possível configurar a estratégia de splitChunks com tipagem segura.

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['./src/utils/parseData.ts']
        }
      }
    }
  }
});
// webpack.config.ts
import type { Configuration } from 'webpack';

const config: Configuration = {
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      maxSize: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        }
      }
    }
  }
};

No Node.js, o tratamento de módulos assíncronos segue o mesmo padrão, mas requer atenção ao ambiente CommonJS vs ESM.

// Node.js com ESM
const loadNodeModule = async () => {
  const fs = await import('fs/promises');
  return fs.readFile('./data.json', 'utf-8');
};

3. Tipagem de Componentes Carregados Dinamicamente (React/Vue)

No React, React.lazy permite carregar componentes dinamicamente com tipagem completa.

import React, { Suspense, lazy } from 'react';

interface DashboardProps {
  userId: string;
  onLogout: () => void;
}

const LazyDashboard = lazy<React.ComponentType<DashboardProps>>(
  () => import('./Dashboard')
);

const App: React.FC = () => {
  return (
    <Suspense fallback={<div>Carregando...</div>}>
      <LazyDashboard userId="123" onLogout={() => {}} />
    </Suspense>
  );
};

No Vue, defineAsyncComponent pode ser tipado com interfaces personalizadas.

import { defineAsyncComponent, type Component } from 'vue';

interface AsyncComponentOptions {
  loader: () => Promise<Component>;
  loadingComponent?: Component;
  errorComponent?: Component;
  delay?: number;
  timeout?: number;
}

function createAsyncComponent<T extends Component>(options: AsyncComponentOptions): T {
  return defineAsyncComponent({
    loader: options.loader,
    loadingComponent: options.loadingComponent,
    errorComponent: options.errorComponent,
    delay: options.delay || 200,
    timeout: options.timeout || 3000
  }) as T;
}

const AsyncUserCard = createAsyncComponent({
  loader: () => import('./UserCard.vue')
});

4. Estratégias de Carregamento com Tipos Genéricos

Uma função utilitária genérica facilita o carregamento lazy com tipagem completa.

type LoadingState<T> = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

async function lazyImport<T>(
  importFn: () => Promise<{ default: T }>,
  onStateChange?: (state: LoadingState<T>) => void
): Promise<T> {
  onStateChange?.({ status: 'loading' });
  try {
    const module = await importFn();
    onStateChange?.({ status: 'success', data: module.default });
    return module.default;
  } catch (error) {
    const typedError = error instanceof Error ? error : new Error(String(error));
    onStateChange?.({ status: 'error', error: typedError });
    throw typedError;
  }
}

// Uso com discriminated union
const [state, setState] = useState<LoadingState<typeof import('./HeavyComponent')>>({
  status: 'idle'
});

useEffect(() => {
  lazyImport(() => import('./HeavyComponent'), setState);
}, []);

5. Tipagem de Rotas com Lazy Loading (React Router / Next.js)

No React Router, rotas lazy-loaded podem ser tipadas com RouteObject.

import type { RouteObject } from 'react-router-dom';

interface LazyRoute extends Omit<RouteObject, 'element' | 'Component'> {
  lazyComponent: () => Promise<{ default: React.ComponentType<any> }>;
}

const createLazyRoute = (route: LazyRoute): RouteObject => ({
  ...route,
  lazy: async () => {
    const { default: Component } = await route.lazyComponent();
    return { Component };
  }
});

const routes: RouteObject[] = [
  createLazyRoute({
    path: '/dashboard',
    lazyComponent: () => import('./pages/Dashboard')
  }),
  createLazyRoute({
    path: '/settings',
    lazyComponent: () => import('./pages/Settings')
  })
];

No Next.js, o code splitting é automático por página, mas você pode tipar explicitamente:

// app/page.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Página Inicial'
};

export default function HomePage() {
  return <div>Home</div>;
}

6. Tratamento de Erros e Fallbacks com Tipos

Erros em imports dinâmicos podem ser tipados para tratamento adequado.

type LazyLoadError = 
  | { type: 'network'; message: string }
  | { type: 'timeout'; message: string }
  | { type: 'module'; originalError: Error };

interface LazyComponentProps<T> {
  loader: () => Promise<{ default: T }>;
  fallback: React.ReactNode;
  errorComponent?: React.ComponentType<{ error: LazyLoadError }>;
}

function LazyLoader<T>({ loader, fallback, errorComponent: ErrorComp }: LazyComponentProps<T>) {
  const [state, setState] = useState<LoadingState<T>>({ status: 'idle' });

  useEffect(() => {
    const load = async () => {
      setState({ status: 'loading' });
      try {
        const result = await loader();
        setState({ status: 'success', data: result.default });
      } catch (err) {
        const lazyError: LazyLoadError = {
          type: 'module',
          originalError: err instanceof Error ? err : new Error(String(err))
        };
        setState({ status: 'error', error: lazyError as any });
      }
    };
    load();
  }, [loader]);

  switch (state.status) {
    case 'loading':
      return <>{fallback}</>;
    case 'success':
      return <state.data />;
    case 'error':
      return ErrorComp ? <ErrorComp error={state.error} /> : <div>Erro ao carregar</div>;
    default:
      return null;
  }
}

7. Testes e Manutenção com Módulos Lazy

Mockar imports dinâmicos em testes preserva a tipagem.

// __mocks__/heavyModule.ts
export const heavyFunction = jest.fn().mockReturnValue('mocked');

// Teste
jest.mock('./heavyModule', () => ({
  __esModule: true,
  default: { heavyFunction: jest.fn() }
}));

import { heavyFunction } from './heavyModule';

test('deve chamar heavyFunction', async () => {
  const module = await import('./heavyModule');
  module.heavyFunction();
  expect(heavyFunction).toHaveBeenCalled();
});

Para análise de tipos em chunks, use a TypeScript Compiler API:

import ts from 'typescript';

function analyzeChunkTypes(filePath: string) {
  const program = ts.createProgram([filePath], {});
  const sourceFile = program.getSourceFile(filePath);
  if (sourceFile) {
    const typeChecker = program.getTypeChecker();
    // Analisar tipos do módulo lazy
    sourceFile.statements.forEach(statement => {
      const type = typeChecker.getTypeAtLocation(statement);
      console.log(type);
    });
  }
}

Boas práticas para evitar perda de inferência:
- Sempre importe módulos com a sintaxe import('./module') em vez de usar strings dinâmicas
- Use tipos genéricos para wrappers lazy
- Evite any em módulos lazy — prefira tipagem explícita com typeof
- Documente interfaces de componentes lazy-loaded para facilitar manutenção

Referências