Delirios de un Informático

Los navegadores y los búferes de salida

Hace unos días me vi en la necesidad de implementar en una web un proceso que puede tardar uno o dos minutos dependiendo de varios factores. El problema reside en que es un proceso que inicia el usuario y el usuario normalmente es impaciente, por lo que es normal encontrarse con que se pulsa F5 (o un montón de teclas al azar) o se cierra el navegador pensando que la aplicación se ha colgado. Por eso pensé en implementar una barra de progreso con JavaScript (utilizando la estupenda librería jQuery UI) en la que el progreso fuera real y fiable. El proceso lento iría mostrando poco a poco el progreso y mediante JavaScript se interpreta y modifica la barra.

Para obtener progresivamente el contenido de una petición realizada con JavaScript hay varias técnicas y tecnologías (HTTP Streaming, Server-Sent Events…) pero opté por algo mucho más sencillo: cargar el script lento en un <iframe> que en teoría debería funcionar para todos los navegadores. Basta hacer que el proceso vaya mostrando poco a poco bloques de información procesable por JavaScript (JSON, por ejemplo) y con un setTimeout() se comprueba el contenido del <iframe>, obteniendo el último bloque. En mi caso, cada bloque contiene el porcentaje y un mensaje para que se vaya mostrando en la barra de progreso. El código PHP podría ser:

echo json_encode(array('percent'=>10,'text'=>'Iniciando el proceso...')); flush();
echo json_encode(array('percent'=>50,'text'=>'Proceso a la mitad...')); flush();
echo json_encode(array('percent'=>100,'text'=>'Proceso finalizado...')); flush();

El problema es sólo Mozilla mostraba el contenido tras cada flush() mientras que los navegadores basados en Webkit así como Opera e Internet Explorer no mostraban nada hasta que terminaba el proceso. Tras mucho curiosear dí con la solución gracias a este comentario en PHP.net que explica que (exceptuando a los navegadores con motor Gecko) los navegadores no mostrarán nada hasta recibir una etiqueta HTML. Por eso, tras cada echo es necesario añadir una etiqueta como <br> que fuerce al navegador a mostrar el contenido. Además, algunas versiones de Internet Explorer no mostrarán nada hasta haber recibido al menos 256 bytes y Firefox no mostrará las líneas con menos de 8 bytes. Así que con las correcciones, el código PHP anterior quedaría así:

echo str_repeat(' ', 256) . '<br>'; flush();
echo json_encode(array('percent'=>10,'text'=>'Iniciando el proceso...')) . '<br>'; flush();
echo json_encode(array('percent'=>50,'text'=>'Proceso a la mitad...')) . '<br>'; flush();
echo json_encode(array('percent'=>100,'text'=>'Proceso finalizado...')) . '<br>'; flush();

Es importante tener en cuenta que Internet Explorer procesa las etiquetas en mayúsculas, por lo que al acceder mediante JavaScript al contenido del marco oculto habrá que tenerlo en cuenta. En mi caso, al utilizar <br> como separador he de realizar la separación de los bloques mediante una expresión regular con el modificador /i.

También hay que destacar que hay ciertas cosas que pueden provocar que todo esto no funcione, como los módulos de compresión de los servidores web o ciertos módulos de seguridad como mod_security. En estos casos no se enviará nada al navegador hasta haberse completado la petición, por lo que no será posible obtener el contenido del marco oculto hasta que el servidor envíe todo de golpe.

4 comentarios en “Los navegadores y los búferes de salida”

andres dice:

Pero el problema de F5 persiste y si es un proceso largo solo puedes controlar el tiempo de respuesta de apache tocando el timeout de la pagina.
supuniendo que tienes el control del servidor, seria mas facil hacerlo por ayax, correr el proceso en segundo plano:

/dev/null &’);
?>

y que este proceso escriba en un archivo log, e ir consultando por ajax este log

Joseph dice:

siempre me he preguntado como hacer eso.
tengo que cargar muchos datos o es una operacion larga, que hace que el timeout del server me pille y me corte a la mitad. (tambien esta el cliente que al ver la pagina en blanco mientras carga, piensa que se cayo y reinicia el sitio)
entonces habia pensado en eso que dice Andres, dejar corriendo un proceso e ir escribiendo en un log y luego ir leyendo ese log, ahora no se si se podra dejar leyendo un archivo, mientras se va llenando. si no habria que ir consultando cada cierto tiempo.

admin dice:

Esto lo he pensado para procesos que tardan 20-30 segundos a lo sumo, no sirve para procesos de minutos. Al ser JavaScript el F5 puedes controlarlo con onBeforeUnload() y similares o capturando las teclas e inhabilitándolas.

Joseph, si es un proceso largo veo más adecuado crear una cola de procesos, en donde se añadan los “trabajos” a realizar y un script en el servidor revise, ejecute y notifique al usuario, porque más de 1 minuto esperando en una web seguro que te provoca problemas…