Módulos y Bundlers
Aprende a organizar y optimizar tu código JavaScript utilizando sistemas de módulos modernos y herramientas de empaquetado (bundlers) para aplicaciones web escalables.
Cristian Escalante
Última actualización: 22 de abril de 2025
Introducción a los módulos en JavaScript
Los módulos son una forma de organizar el código en archivos separados, permitiendo una mejor estructura, reutilización y mantenimiento del código.
¿Por qué necesitamos módulos?
En los primeros días de JavaScript, todo el código se escribía en un solo archivo o se dividía en varios archivos que compartían el mismo ámbito global. Esto generaba varios problemas:
- Colisiones de nombres: Variables y funciones con el mismo nombre podían sobrescribirse.
- Dependencias implícitas: Era difícil saber qué archivos dependían de otros.
- Mantenimiento complicado: Los proyectos grandes se volvían difíciles de mantener.
- Dificultad para reutilizar código: No existía una forma estándar de compartir código entre proyectos.
Beneficios de los módulos
- Encapsulamiento: Las variables y funciones están limitadas al ámbito del módulo.
- Reutilización: El código puede reutilizarse fácilmente en diferentes partes de la aplicación.
- Dependencias explícitas: Cada módulo declara claramente de qué otros módulos depende.
- Organización: El código se organiza de forma lógica y estructurada.
- Mantenimiento: Es más fácil mantener, probar y depurar módulos individuales.
Evolución de los sistemas de módulos
IIFE (Immediately Invoked Function Expressions)
Antes de los sistemas de módulos formales, se usaban IIFE para simular módulos:
// Módulo usando IIFE
var miModulo = (function() {
// Variables privadas
var contador = 0;
// Funciones privadas
function incrementar() {
contador++;
}
// API pública
return {
incrementar: incrementar,
obtenerContador: function() {
return contador;
}
};
})();
// Uso
miModulo.incrementar();
console.log(miModulo.obtenerContador()); // 1
CommonJS
Popularizado por Node.js, utiliza require()
y module.exports
:
// matematica.js
function sumar(a, b) {
return a + b;
}
function restar(a, b) {
return a - b;
}
module.exports = {
sumar,
restar
};
// app.js
const matematica = require('./matematica');
console.log(matematica.sumar(5, 3)); // 8
AMD (Asynchronous Module Definition)
Diseñado para navegadores, permite cargar módulos de forma asíncrona:
// Definir un módulo
define('miModulo', ['dependencia1', 'dependencia2'],
function(dependencia1, dependencia2) {
return {
metodo: function() {
return dependencia1.algo() + dependencia2.otro();
}
};
}
);
// Usar un módulo
require(['miModulo'], function(miModulo) {
miModulo.metodo();
});
UMD (Universal Module Definition)
Combina CommonJS y AMD para funcionar en múltiples entornos:
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['dependencia'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory(require('dependencia'));
} else {
// Global
root.miModulo = factory(root.dependencia);
}
}(typeof self !== 'undefined' ? self : this, function(dependencia) {
return {
// Implementación del módulo
};
}));
ES Modules (ESM)
El sistema de módulos nativo de JavaScript, introducido en ES6 (ES2015):
// matematica.js
export function sumar(a, b) {
return a + b;
}
export function restar(a, b) {
return a - b;
}
// app.js
import { sumar, restar } from './matematica.js';
console.log(sumar(5, 3)); // 8
ES Modules en detalle
Sintaxis básica de exportación
// Exportaciones con nombre
export const PI = 3.14159;
export function calcularArea(radio) {
return PI * radio * radio;
}
// Exportación por defecto (solo una por archivo)
export default class Circulo {
constructor(radio) {
this.radio = radio;
}
obtenerArea() {
return PI * this.radio * this.radio;
}
}
// Exportaciones agrupadas
const PI = 3.14159;
function calcularArea(radio) {
return PI * radio * radio;
}
export { PI, calcularArea };
Sintaxis básica de importación
// Importar exportaciones con nombre
import { PI, calcularArea } from './matematica.js';
// Importar con alias
import { PI as CONSTANTE_PI, calcularArea } from './matematica.js';
// Importar todo como un objeto
import * as Matematica from './matematica.js';
console.log(Matematica.PI); // 3.14159
// Importar exportación por defecto
import Circulo from './circulo.js';
const miCirculo = new Circulo(5);
// Importar exportación por defecto y con nombre
import Circulo, { PI } from './circulo.js';
Re-exportación
// Re-exportar todo de otro módulo
export * from './matematica.js';
// Re-exportar elementos específicos
export { sumar, restar } from './matematica.js';
// Re-exportar con alias
export { sumar as suma, restar as resta } from './matematica.js';
// Re-exportar exportación por defecto
export { default } from './matematica.js';
export { default as Matematica } from './matematica.js';
Importaciones dinámicas
Las importaciones dinámicas permiten cargar módulos bajo demanda:
// Importación estática (se carga al inicio)
import { funcionA } from './moduloA.js';
// Importación dinámica (se carga cuando se necesita)
async function cargarModuloCuandoSeaNecesario() {
try {
// Devuelve una promesa
const modulo = await import('./moduloGrande.js');
modulo.funcionEspecial();
} catch (error) {
console.error('Error al cargar el módulo:', error);
}
}
// Con then/catch
document.getElementById('boton').addEventListener('click', () => {
import('./moduloGrande.js')
.then(modulo => {
modulo.funcionEspecial();
})
.catch(error => {
console.error('Error al cargar el módulo:', error);
});
});
Módulos en el navegador
Para usar ES Modules directamente en el navegador:
<!-- Indicar que es un módulo con type="module" -->
<script type="module">
import { sumar } from './matematica.js';
console.log(sumar(2, 3));
</script>
<!-- También funciona con archivos externos -->
<script type="module" src="app.js"></script>
Características importantes de los módulos en el navegador:
- Ámbito estricto: Los módulos siempre se ejecutan en modo estricto.
- Diferido por defecto: Los módulos se cargan como si tuvieran el atributo
defer
. - CORS: Las solicitudes de módulos respetan las políticas de CORS.
- Cargados una vez: Cada módulo se evalúa solo una vez, sin importar cuántas veces se importe.
- Sin acceso global: Las variables definidas en módulos no contaminan el ámbito global.
Bundlers (Empaquetadores)
Los bundlers son herramientas que toman múltiples archivos JavaScript y sus dependencias, y los combinan en uno o varios archivos optimizados para producción.
¿Por qué usar bundlers?
- Rendimiento: Reducen el número de solicitudes HTTP combinando archivos.
- Compatibilidad: Transforman código moderno en código compatible con navegadores antiguos.
- Optimización: Minimizan y optimizan el código para producción.
- Gestión de dependencias: Resuelven y gestionan dependencias automáticamente.
- Procesamiento de recursos: Permiten importar CSS, imágenes y otros recursos no JavaScript.
Principales bundlers
Webpack
El bundler más popular y completo:
// webpack.config.js básico
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};
Rollup
Especializado en bibliotecas, con excelente tree-shaking:
// rollup.config.js
import babel from '@rollup/plugin-babel';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/main.js',
output: {
file: 'dist/bundle.js',
format: 'esm', // o 'cjs', 'umd', 'iife'
},
plugins: [
resolve(),
commonjs(),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
}),
],
};
Parcel
Configuración cero, muy fácil de usar:
# Instalación
npm install -g parcel-bundler
# Uso (sin archivo de configuración)
parcel index.html
esbuild
Extremadamente rápido, escrito en Go:
// esbuild.config.js
require('esbuild').build({
entryPoints: ['src/index.js'],
bundle: true,
minify: true,
sourcemap: true,
target: ['chrome58', 'firefox57', 'safari11', 'edge16'],
outfile: 'dist/bundle.js',
}).catch(() => process.exit(1));
Vite
Moderno, rápido, optimizado para desarrollo:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
},
});
Webpack en detalle
Webpack es el bundler más utilizado en proyectos JavaScript modernos. Veamos cómo configurarlo y utilizarlo.
Instalación básica
# Crear package.json
npm init -y
# Instalar webpack
npm install --save-dev webpack webpack-cli
# Opcional: instalar servidor de desarrollo
npm install --save-dev webpack-dev-server
Conceptos clave de Webpack
- Entry: Punto(s) de entrada de la aplicación
- Output: Dónde y cómo se generarán los archivos
- Loaders: Transforman archivos que no son JavaScript
- Plugins: Realizan tareas más complejas
- Mode: Modo de construcción (development, production, none)
Configuración completa
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
// Punto de entrada
entry: './src/index.js',
// Salida
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? 'js/[name].[contenthash].js' : 'js/[name].js',
publicPath: '/',
},
// Modo
mode: isProduction ? 'production' : 'development',
// Mapas de origen
devtool: isProduction ? 'source-map' : 'eval-source-map',
// Servidor de desarrollo
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
hot: true,
historyApiFallback: true,
},
// Reglas para diferentes tipos de archivos
module: {
rules: [
// JavaScript con Babel
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
// CSS y SASS
{
test: /\.(css|scss)$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'postcss-loader',
'sass-loader',
],
},
// Imágenes
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
generator: {
filename: 'images/[hash][ext][query]',
},
},
// Fuentes
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[hash][ext][query]',
},
},
],
},
// Plugins
plugins: [
// Limpia la carpeta dist antes de construir
new CleanWebpackPlugin(),
// Genera el HTML
new HtmlWebpackPlugin({
template: './src/index.html',
minify: isProduction,
}),
// Extrae CSS a archivos separados en producción
...(isProduction ? [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
}),
] : []),
],
// Optimizaciones
optimization: {
// Divide el código
splitChunks: {
chunks: 'all',
name: false,
},
},
// Resolución de módulos
resolve: {
extensions: ['.js', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
};
};
Scripts en package.json
{
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production",
"analyze": "webpack --mode production --analyze"
}
}
Técnicas avanzadas de optimización
Code Splitting
Divide el código en múltiples archivos para cargar solo lo necesario:
// Webpack config para code splitting
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// Obtén el nombre del paquete
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
};
// En el código: importaciones dinámicas
const ComponenteLazy = React.lazy(() => import('./ComponentePesado'));
function App() {
return (
<React.Suspense fallback={<div>Cargando...</div>}>
<ComponenteLazy />
</React.Suspense>
);
}
Tree Shaking
Elimina código no utilizado del bundle final:
// modulo.js - solo se incluirá funcionUsada en el bundle final
export function funcionUsada() {
return 'Esta función se usa';
}
export function funcionNoUsada() {
return 'Esta función no se incluirá en el bundle';
}
// app.js
import { funcionUsada } from './modulo.js';
funcionUsada();
Para que el tree shaking funcione correctamente:
- Usar ES Modules (import/export)
- Evitar efectos secundarios en los módulos
- Configurar correctamente el bundler
- En webpack, usar
mode: 'production'
o configuraroptimization.usedExports: true
Lazy Loading
Cargar módulos solo cuando se necesitan:
// Rutas con lazy loading en React
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Contact = lazy(() => import('./routes/Contact'));
function App() {
return (
<Router>
<Suspense fallback={<div>Cargando...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Switch>
</Suspense>
</Router>
);
}
Preloading y Prefetching
Técnicas para mejorar el rendimiento percibido:
// Prefetch: carga en segundo plano cuando el navegador está inactivo
import(/* webpackPrefetch: true */ './modulo-futuro.js');
// Preload: carga con mayor prioridad
import(/* webpackPreload: true */ './modulo-necesario-pronto.js');
Mejores prácticas
- Estructura de proyecto clara:
/proyecto /src /components /utils /styles index.js /dist package.json webpack.config.js
- Separar código por responsabilidades:
// api.js - Maneja llamadas a API export async function fetchUsuarios() { // ... } // utils.js - Funciones de utilidad export function formatearFecha(fecha) { // ... } // componente.js - Lógica de UI import { fetchUsuarios } from './api.js'; import { formatearFecha } from './utils.js';
- Usar alias para importaciones:
// webpack.config.js module.exports = { // ... resolve: { alias: { '@': path.resolve(__dirname, 'src'), '@components': path.resolve(__dirname, 'src/components'), '@utils': path.resolve(__dirname, 'src/utils'), } } } // En el código import Button from '@components/Button'; import { formatDate } from '@utils/dates';
- Analizar el tamaño del bundle:
# Instalar npm install --save-dev webpack-bundle-analyzer # En webpack.config.js const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { // ... plugins: [ // ... new BundleAnalyzerPlugin() ] }
- Dividir configuración por entorno:
/config webpack.common.js webpack.dev.js webpack.prod.js
// webpack.common.js const path = require('path'); module.exports = { entry: './src/index.js', module: { rules: [ // Reglas comunes ] } }; // webpack.dev.js const { merge } = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'development', devtool: 'inline-source-map', devServer: { // Configuración del servidor de desarrollo } }); // webpack.prod.js const { merge } = require('webpack-merge'); const common = require('./webpack.common.js'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = merge(common, { mode: 'production', devtool: 'source-map', plugins: [ new MiniCssExtractPlugin() ] });
Ejemplos prácticos
Proyecto básico con Webpack
# Estructura de directorios
mkdir mi-proyecto && cd mi-proyecto
npm init -y
mkdir src dist
# Instalar dependencias
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev babel-loader @babel/core @babel/preset-env
npm install --save-dev css-loader style-loader
npm install --save-dev html-webpack-plugin
// src/utils.js
export function saludar(nombre) {
return `¡Hola, ${nombre}!`;
}
// src/index.js
import { saludar } from './utils.js';
import './styles.css';
document.getElementById('app').innerHTML = saludar('Mundo');
// src/styles.css
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
}
#app {
padding: 20px;
background-color: white;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
margin: 20px auto;
max-width: 600px;
}
// src/index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mi Proyecto</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
port: 8080,
open: true
}
};
// package.json (scripts)
{
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production"
}
}
Proyecto con múltiples entradas
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js',
admin: './src/admin.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
},
// ...
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
chunks: ['main'],
filename: 'index.html'
}),
new HtmlWebpackPlugin({
template: './src/admin.html',
chunks: ['admin'],
filename: 'admin.html'
})
],
// ...
};
Herramientas complementarias
Babel
Transpila código JavaScript moderno a versiones compatibles con navegadores antiguos:
npm install --save-dev babel-loader @babel/core @babel/preset-env
// .babelrc
{
"presets": [
["@babel/preset-env", {
"targets": "> 0.25%, not dead",
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
ESLint
Analiza el código para encontrar problemas:
npm install --save-dev eslint eslint-webpack-plugin
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: 'eslint:recommended',
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
rules: {
// Reglas personalizadas
}
};
// webpack.config.js
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
// ...
plugins: [
// ...
new ESLintPlugin()
]
};
PostCSS
Procesa CSS con plugins como Autoprefixer:
npm install --save-dev postcss postcss-loader autoprefixer
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
};
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}
]
}
};