Skip to main content

Soluciones de almacenamiento de imágenes para SaaS: Local vs S3 vs CDN (Guía 2025)

Comparativa completa entre almacenamiento local, AWS S3 y CDN para aplicaciones SaaS. Incluye ejemplos de React y Next.js, ideal para productos que dividen o procesan imágenes.

2025-07-17
Soluciones de almacenamiento de imágenes para SaaS: Local vs S3 vs CDN (Guía 2025)

Soluciones de almacenamiento de imágenes para SaaS

Elegir dónde y cómo guardar las imágenes determina el rendimiento y la escalabilidad de tu producto SaaS. A continuación comparamos almacenamiento local, AWS S3 y una capa CDN, con ejemplos reales para React y Next.js.

S3
Photo AWS

¿Por qué importa la estrategia?

  • Rendimiento: tiempos de carga y Core Web Vitals
  • Costes: almacenamiento + transferencia + operaciones
  • Escalabilidad: soportar picos de tráfico y GBs de datos
  • Confiabilidad: tolerancia a fallos, backups y recuperación
  • Cumplimiento: residencias de datos y políticas internas

Almacenamiento local

Cuándo usarlo

  • MVP, prototipos rápidos
  • Aplicaciones pequeñas o entornos de desarrollo
  • Requisitos on-premise

Limitaciones

  • Espacio de disco limitado
  • Punto único de fallo
  • Backups manuales
  • Sin distribución geográfica

Ejemplo en React

Estructura sugerida

src/
├── components/
│   ├── ImageUpload.jsx
│   ├── ImageDisplay.jsx
│   └── ImageGallery.jsx
├── services/
│   ├── imageService.js
│   └── storageService.js
└── utils/

Uploader

function ImageUpload({ onUploadSuccess, category = 'general' }) {
  // mismo componente que en el artículo original
}

Backend Express

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const category = req.body.category || 'general';
    const uploadPath = path.join(__dirname, 'public/uploads', category);
    if (!fs.existsSync(uploadPath)) fs.mkdirSync(uploadPath, { recursive: true });
    cb(null, uploadPath);
  },
  filename: (req, file, cb) => {
    const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, `${unique}${path.extname(file.originalname)}`);
  }
});

API Next.js (pages/api/upload.js)

export const config = { api: { bodyParser: false } };

export default async function handler(req, res) {
  // usar formidable, mover el archivo a /public/uploads y generar thumbnails con sharp
}

AWS S3

Cuándo migrar a S3

  • Necesitas escalabilidad infinita
  • Usuarios distribuidos globalmente
  • Buscas durabilidad (11 nueves)
  • Prefieres pagar solo por lo que usas

Ventajas

  • Replicación multi AZ
  • Integración con servicios AWS
  • Coste por GB muy bajo
  • Versionado, lifecycle policies, etc.

Configuración base

Bucket Policy

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::tu-bucket/*"
  }]
}

CORS

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET","PUT","POST","DELETE"],
    "AllowedOrigins": ["https://tu-dominio.com"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

Servicio React

class S3Service {
  constructor() {
    this.s3 = new AWS.S3({
      accessKeyId: process.env.REACT_APP_AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.REACT_APP_AWS_SECRET_ACCESS_KEY,
      region: process.env.REACT_APP_AWS_REGION
    });
    this.bucketName = process.env.REACT_APP_S3_BUCKET_NAME;
  }
  uploadFile(file, key) {
    return this.s3.upload({
      Bucket: this.bucketName,
      Key: key,
      Body: file,
      ContentType: file.type,
      ACL: 'public-read'
    }).promise();
  }
}

API Next.js (pages/api/s3-upload.js)

export const config = { api: { bodyParser: false } };

export default async function handler(req, res) {
  // analizar con formidable, subir a S3 y generar miniaturas con sharp
}

Componente de imagen

function S3Image({ src, alt, width, height, fallback = '/placeholder.jpg', ...props }) {
  const [error, setError] = useState(false);
  const [loading, setLoading] = useState(true);
  const url = error || src.startsWith('http')
    ? (error ? fallback : src)
    : `https://${process.env.NEXT_PUBLIC_S3_BUCKET_NAME}.s3.${process.env.NEXT_PUBLIC_AWS_REGION}.amazonaws.com/${src}`;
  return (
    <div className="s3-image-container">
      {loading && <div className="image-skeleton"><div className="skeleton-placeholder" /></div>}
      <Image
        src={url}
        alt={alt}
        width={width}
        height={height}
        onError={() => setError(true)}
        onLoad={() => setLoading(false)}
        style={{ display: loading ? 'none' : 'block' }}
        {...props}
      />
    </div>
  );
}

CDN (CloudFront + S3)

Cuándo añadir CDN

  • Audiencia global
  • Necesitas bajar la latencia
  • Tráfico alto
  • Quieres mejorar Core Web Vitals

Configuración tipo

const cloudfrontConfig = {
  distributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID,
  domainName: process.env.CLOUDFRONT_DOMAIN_NAME,
  origins: [{
    domainName: `${process.env.S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com`,
    originPath: '/images'
  }],
  defaultCacheBehavior: {
    targetOriginId: 'S3-origin',
    viewerProtocolPolicy: 'redirect-to-https',
    cachePolicyId: 'Managed-CachingOptimized',
    compress: true
  }
};

Servicio para URLs optimizadas

class CDNService {
  constructor() { this.domain = process.env.REACT_APP_CLOUDFRONT_DOMAIN; }
  getUrl(key, { width, height, quality = 85, format = 'auto' } = {}) {
    const params = new URLSearchParams();
    if (width) params.append('width', width);
    if (height) params.append('height', height);
    params.append('quality', quality);
    params.append('format', format);
    return `https://${this.domain}/${key}?${params.toString()}`;
  }
}

Buenas prácticas de caché

  • Cache-Control: public, max-age=31536000, immutable
  • Service Workers para caching offline
  • Lambda@Edge si necesitas redimensionar al vuelo

Costes comparados

| Concepto | Local | S3 | CloudFront | | --- | --- | --- | --- | | Almacenamiento | Coste fijo del servidor | 0.023 USD/GB/mes | - | | Requests | - | GET 0.0004 USD/1k | 0.0075 USD/10k | | Transferencia | Depende del hosting | 0.09 USD/GB (primeros 10 TB) | 0.085 USD/GB (primeros 10 TB) | | Mantenimiento | Backups manuales | Lifecycle, versionado | Origen protegido, compresión |

Optimización de costes

  • Lifecycle rules (Standard-IA/Glacier)
  • Intelligent Tiering para mover objetos automáticamente
  • Comprimir imágenes antes de subirlas

Seguridad y validación

  • Políticas IAM estrictas (solo roles necesarios)
  • URLs prefirmadas para cargas seguras
  • Validar tipo y tamaño (jpeg/png/webp, máx. 10 MB)
  • Verificar magic numbers para evitar archivos maliciosos
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_FILE_SIZE = 10 * 1024 * 1024;

Monitorización

  • PerformanceObserver para medir tiempos de carga de imágenes
  • CloudWatch Metrics (BucketSizeBytes, NumberOfObjects)
  • Registro de errores (img.onerror) hacia tu sistema de logging

Recomendaciones finales

| Etapa | Solución | | --- | --- | | MVP / prototipo | Almacenamiento local | | Escalando usuarios | Migrar a S3 | | Rendimiento global | S3 + CDN |

  1. Empieza simple y evoluciona.
  2. Migra a S3 cuando los uploads o el tráfico lo exijan.
  3. Activa CDN para reducir latencia y proteger el origin.
  4. Automatiza la generación de miniaturas y la limpieza de archivos.
  5. Supervisa costos y rendimiento regularmente.

¿Listo para optimizar tus imágenes? Usa nuestras herramientas gratuitas para dividir, comprimir y entregar imágenes hiper rápidas en tu SaaS.

Artículos relacionados

Ready to Try?

Experience it yourself with our tool below

Descubre nuestras herramientas