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)

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

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
Advertisements