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
- MDN: import() expression — Documentação oficial sobre importação dinâmica em JavaScript/TypeScript
- TypeScript Handbook: Dynamic Import Expressions — Guia oficial do TypeScript sobre expressões de importação dinâmica
- Webpack: Code Splitting — Documentação oficial do Webpack sobre code splitting e lazy loading
- Vite: Code Splitting — Guia do Vite sobre importação dinâmica e code splitting
- React: Code-Splitting with React.lazy — Documentação oficial do React sobre lazy loading de componentes
- Vue: defineAsyncComponent — Documentação oficial do Vue sobre componentes assíncronos e lazy loading
- React Router: Lazy Loading Routes — Guia oficial do React Router para rotas lazy-loaded