Ir al contenido
HexaTransfer
Volver al blog
Analisis tecnicos

Subida progresiva de archivos: guía técnica y de UX

Crea experiencias de subida progresiva con arrastrar y soltar, barras de progreso y manejo elegante de errores para una mejor experiencia de usuario.

Una subida de archivos progresiva ofrece al usuario retroalimentación inmediata y fiable en cada etapa: en el instante en que suelta un archivo, una confirmación de selección; durante la subida, una barra de progreso suave con un tiempo estimado realista; ante un fallo, opciones específicas de reintento; tras completarse, un estado de éxito claro con las siguientes acciones. Los ingredientes técnicos son arrastrar y soltar mediante la API HTML5 DataTransfer, subidas fragmentadas con reanudación, streaming con fetch y ReadableStream para progreso exacto en bytes, y gestión de estado que sobrevive a recargas de pestaña mediante IndexedDB. Bien ejecutado, un usuario subiendo un archivo de 5 GB nunca se pregunta si la aplicación está bloqueada.

Qué significa realmente "progresivo" aquí

Progresivo tiene dos significados en este contexto. Uno: mejora progresiva, para que la subida funcione como un POST con <input type="file"> simple en un navegador de 2012, y gane arrastrar y soltar, fragmentación y reintentos cuando JavaScript está disponible. Dos: revelación progresiva, donde la interfaz expone complejidad solo cuando es necesaria: muestra un porcentaje durante la subida, pero muestra detalles de reintento solo ante fallos. Ambos significados apuntan al mismo principio: el usuario nunca debe llegar a un callejón sin salida, ni esperar sin información.

El modo de fallo a evitar es el "spinner de la condena": una animación de carga genérica que no da ninguna indicación de progreso, tiempo estimado o si algo fue mal. Los usuarios cancelan las subidas en las que no confían.

Arrastrar y soltar que no lucha contra el navegador

La API de arrastrar y soltar de HTML5 es famosa por sus errores. Unas pocas reglas la hacen tolerable:

const dropzone = document.querySelector('.dropzone');
dropzone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropzone.classList.add('dragging');
});
dropzone.addEventListener('dragleave', () => {
  dropzone.classList.remove('dragging');
});
dropzone.addEventListener('drop', (e) => {
  e.preventDefault();
  dropzone.classList.remove('dragging');
  handleFiles([...e.dataTransfer.files]);
});

Llama a preventDefault en dragover o la zona de drop no aceptará el archivo. Usa e.dataTransfer.items en lugar de files si necesitas aceptar carpetas mediante webkitGetAsEntry(): es la única forma de capturar recursivamente el contenido de directorios en Chrome y Firefox.

Haz que el fallback también sea usable: un <label> visible que envuelva un <input type="file" multiple> con estilo funciona para el 100% de los usuarios, incluidos los de teclado y lector de pantalla.

Mostrar progreso en el que los usuarios confían

Las barras de progreso saltan por tres razones: tamaños de fragmento desiguales, TCP slow-start y buffering en la pila de red. Suavízalas con una media móvil de 2 segundos:

const samples = []; // [{ time, bytes }]
function recordSample(bytes) {
  const now = performance.now();
  samples.push({ time: now, bytes });
  while (samples.length > 1 && now - samples[0].time > 2000) samples.shift();
}
function throughput() {
  if (samples.length < 2) return 0;
  const delta = samples[samples.length - 1];
  const base = samples[0];
  return (delta.bytes - base.bytes) / ((delta.time - base.time) / 1000);
}

Calcula el tiempo estimado como (totalBytes - uploadedBytes) / throughput(), limita la visualización a un mínimo de 5 segundos y formátalo en términos humanos: "unos 2 minutos" no "124,3 segundos". Muestra tanto un porcentaje como un contador de bytes ("340 MB de 2,1 GB"): los usuarios comprueban los dos cuando algo les parece raro.

Estados de error con recuperación accionable

Los mensajes genéricos de "subida fallida" destruyen la confianza. Clasifica los fallos en cinco categorías y muestra cada una de forma distinta:

  • Corte de red (evento offline, reset TCP): "Reconectando..." con reintento automático
  • Servidor 5xx: "Error del servidor, reintentando en 5 s" con botón de reintento manual
  • Servidor 4xx (413 demasiado grande, 415 tipo incorrecto): "Archivo rechazado: demasiado grande" con reemplazo de archivo
  • Sesión caducada (401, 403): "Sesión expirada, inicia sesión para continuar"
  • Crash del cliente (error JS, pestaña cerrada por el navegador): Recuperación desde IndexedDB al recargar

Empareja el mensaje con la única acción que lo resuelve. Si el usuario está sin conexión, muestra el estado de conexión monitorizado mediante navigator.onLine y el evento online.

Seguimiento de bytes con streams de fetch

XMLHttpRequest.upload.onprogress ha sido la forma tradicional de rastrear el progreso de subida, pero es inestable en HTTP/3 y pierde bytes encolados en el buffer de envío. El enfoque moderno usa ReadableStream para contar bytes a medida que se producen:

function trackedStream(blob, onBytes) {
  let sent = 0;
  return new ReadableStream({
    async pull(controller) {
      const reader = blob.stream().getReader();
      while (true) {
        const { done, value } = await reader.read();
        if (done) { controller.close(); return; }
        sent += value.byteLength;
        onBytes(sent);
        controller.enqueue(value);
      }
    }
  });
}

Pasa el stream como body a fetch con duplex: 'half'. El soporte de Safari para streams de petición llegó en la versión 17.4; antes de eso, recurre a XMLHttpRequest. Esto te da progreso exacto al milisegundo vinculado a bytes reales entregados a la pila de red.

Pausar, reanudar y cancelar

Los usuarios esperan un botón de pausa en cualquier cosa que tarde más de un minuto. Con subidas fragmentadas, pausar es simplemente "dejar de despachar nuevos fragmentos", y reanudar sigue desde donde se dejó la cola de trabajo. La cancelación usa AbortController:

const ctrl = new AbortController();
cancelButton.onclick = () => ctrl.abort();
await fetch(url, { method: 'PUT', body: blob, signal: ctrl.signal });

Al cancelar, haz limpieza: DELETE la sesión de subida en el servidor para que el almacenamiento no se acumule, limpia la entrada de IndexedDB, vuelve al estado inicial. Pausar debe preservar el estado; cancelar debe destruirlo. Haz visible la distinción en la interfaz.

Sobrevivir a recargas de pestaña

Persiste el estado de subida tras cada fragmento completado:

await idb.put('uploads', {
  sessionId, fileFingerprint, fileName, fileSize,
  completedChunks: [...done], updatedAt: Date.now()
}, sessionId);

La huella digital es un SHA-256 del primer MB del archivo más el tamaño y lastModified: suficiente para re-identificar el archivo cuando el usuario lo selecciona de nuevo tras recargar. Al cargar la página, comprueba IndexedDB en busca de sesiones de menos de una hora y ofrece reanudación: "Tienes una subida en progreso de hace 12 minutos. ¿Reanudar?" No reanudes automáticamente sin consentimiento: los usuarios a veces recargan específicamente para cancelar.

Interacciones accesibles y compatibles con teclado

Una zona de drop que solo responde al ratón falla a los usuarios de lector de pantalla y teclado. Añade:

  • role="button" y tabindex="0" en la zona de drop
  • Manejador de teclas Enter/Espacio que hace clic en el input de archivo
  • aria-live="polite" en la región de progreso para que los lectores de pantalla anuncien hitos
  • Estilos de foco visibles, no solo hover
  • Etiquetas claras: "Subir archivo" supera a "Examinar" que supera a un icono desnudo

La prueba de teclado es rápida: desconecta el ratón 10 minutos e intenta completar una subida. Si no puedes, tampoco podrá una parte de tus usuarios.

HexaTransfer usa exactamente este patrón progresivo: fallback de formulario simple, mejora con arrastrar y soltar, streaming con fetch, reanudación respaldada por IndexedDB y recuperación de errores específica. Pruébalo en https://hexatransfer.com — gratuito, sin cuenta, hasta 10 GB.

Los detalles que los usuarios realmente notan

El pulido que separa los cargadores memorables de los mediocres vive en pequeños momentos: una animación de drop que confirma que se capturó el archivo, una barra de progreso que se llena suavemente en lugar de saltar, un tiempo estimado que mejora su precisión con el tiempo en lugar de fluctuar, mensajes de error específicos que dicen qué hacer a continuación, una pregunta de reanudación tras un refresco accidental, un estado de finalización que persiste el tiempo suficiente para copiar el enlace de compartición, y un comportamiento de cancelación que detiene la subida de forma inmediata. Cada uno de estos detalles son unas pocas líneas de código. Impleméntalos todos y tu cargador se sentirá un orden de magnitud mejor que el tratamiento predeterminado del <input type="file">.

Envía archivos grandes de forma segura con cifrado de extremo a extremo

Transfiere archivos de hasta 10 GB gratis con cifrado de extremo a extremo. Sin necesidad de cuenta. Tus archivos se cifran en tu navegador antes de subirlos: nadie más puede leerlos.

Enviar un archivo