Una de las reglas de oro cuando tratamos de mejorar el rendimiento en aplicaciones Web en reducir el uso de peticiones HTTP. Normalmente solemos tener varios archivos Javascript dentro de nuestros proyectos, lo que significa varias peticiones HTTP para descargarlos. Es una práctica recomendable unir todos estos archivos JavaScript en uno solo para reducir el número de peticiones. En este artículo veremos cómo hacer esto usando un script PHP.

Autor:

Desarrollador y arquitecto Web con más de 10 años de experiencia, especializado en tecnologías Open Source. Experiencia con desarrollos Web en entornos industriales. Puedes leerle en su blog gonzalo123.wordpress.com y también seguirle en Twitter @gonzalo123.
Descarga Demostración

Introducción

Una de las reglas de oro cuando tratamos de mejorar el rendimiento en aplicaciones Web es reducir el uso de peticiones HTTP. Normalmente solemos tener varios archivos Javascript dentro de nuestros proyectos, lo que significa varias peticiones HTTP para descargarlos.

Es una práctica recomendable unir todos estos archivos Javascript en uno solo para reducir el número de peticiones. Podemos hacerlo de forma manual. No es difícil. Solo tenemos que cortar y pegar las fuentes Javascript en un único archivo. Incluso tenemos herramientas que nos ayudan a hacerlo, como Yslow.

Esto es una buena solución si nuestro proyecto está terminado. Pero si nuestro proyecto está vivo y lo estamos modificando, suele ser útil (al menos para mí) separar los archivos Javascript para tener el código más organizado y mantenerlo.

En definitiva, que tenemos que decidir entre el rendimiento de nuestra aplicación en producción o ayudar al desarrollador. Es por esto que me gusta usar el script que os voy a mostrar. Un script PHP que junta todos los archivos .js dentro de un único archivo de forma dinámica.

¿Cómo aceleramos los tiempos de carga de Javascript?

La idea es la siguiente. Normalmente suelo tener los archivos .js guardados jerárquicamente dentro de una carpeta que se llama js (¿original, no?). También suelo tener un servidor de desarrollo y uno de producción (¿realmente original, no?). Cuando estoy desarrollando mi aplicación me gusta separar los archivos y en un formato legible, pero cuando están en producción combinarlos en uno solo e incluso minimizarlos para mejorar el tiempo de descarga.

El script que tengo para combinar todos los archivos .js en uno solo es el siguiente:

//js.php
require 'jsmin.php';

function checkCanGzip(){
    if (array_key_exists('HTTP_ACCEPT_ENCODING', $_SERVER)) {
        if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) return "gzip";
        if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== false) return "x-gzip";
    }
    return false;
}

function gzDocOut($contents, $level=6){
    $return = array();
    $return[] = "\x1f\x8b\x08\x00\x00\x00\x00\x00";
    $size = strlen($contents);
    $crc = crc32($contents);
    $contents = gzcompress($contents,$level);
    $contents = substr($contents, 0, strlen($contents) - 4);
    $return[] = $contents;
    $return[] = pack('V',$crc);
    $return[] = pack('V',$size);
    return implode(null, $return);
}

$ite = new RecursiveDirectoryIterator(dirname(__FILE__));
foreach(new RecursiveIteratorIterator($ite) as $file => $fileInfo) {
    $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
    if ($extension == 'js') {
        $f = $fileInfo->openFile('r');
        $fdata = "";
        while ( ! $f->eof()) {
            $fdata .= $f->fgets();
        }
        $buffer[] = $fdata;
    }
}

$output = JSMin::minify(implode(";\n", $buffer));

header("Content-type: application/x-javascript; charset: UTF-8");
$forceGz    = filter_input(INPUT_GET, 'gz', FILTER_SANITIZE_STRING);
$forcePlain = filter_input(INPUT_GET, 'plain', FILTER_SANITIZE_STRING);

$encoding = checkCanGzip();
if ($forceGz) {
    header("Content-Encoding: {$encoding}");
    echo gzDocOut($output);
} elseif ($forcePlain) {
    echo $output;
} else {
    if ($encoding){
        header("Content-Encoding: {$encoding}");
        echo GzDocOut($output);
    } else {
        echo $output;
    }
}

Como podemos ver el script se recorre recursivamente los archivos Javascript dentro de una carpeta, los une e incluso le pasa la librería jsmin para PHP para reducir el tamaño del archivo Javascript resultante, y mejorar así el tiempo total de descarga en el navegador.

Con este script en marcha, es muy sencillo crear un archivo HTML que dependiendo del entorno en el que nos encontremos (producción o desarrollo) incluya los tags HTML necesarios para usar la versión minimizada y unificada o todos los archivos Javascript por separado. Aquí pongo un ejemplo usando el motor de plantillas Smarty:

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title></title>
    </head>
    <body>
        Hello World
{if $dev}
        <script src="js1.js" type="text/javascript"></script>
        <script src="js2.js" type="text/javascript"></script>
        <script src="xxx/js1.js" type="text/javascript"></script>
{else}
        <script src="js.php" type="text/javascript"></script>
{/if}
    </body>
</html>

Sí, lo sé. Hay un problema con esta solución. Quizás hemos mejorado el rendimiento en el cliente (navegador) reduciendo el número de peticiones HTTP, pero ¿Qué pasa con el rendimiento en servidor? Hemos pasado de servir archivos Javascript estáticos, a necesitar PHP para generar archivos estáticos. Ahora el servidor consume más CPU. Dependiendo del hosting que estemos usando, más CPU supone más caro.

Alojar los recursos y archivos estáticos en servidores independientes al servidor web nos permite descargar en paralelo de forma más rápida y eficiente.

Otra regla de oro del rendimiento Web es poner el contenido estático en un servidor dedicado, o CDN sin PHP. Con esta segunda regla de oro conseguimos realizar descargar múltiples más eficientes de nuestro contenido estático y reducir el consumo de CPU, ya que los servidores que sirven contenido estático no necesitan crear una instancia de PHP ni interpretar ningún código.

Por lo tanto una mejor solución en la siguiente: Generamos el archivo Javascript estático de forma offline cuando desplegamos nuestra aplicación a producción. Yo lo suelo hacer con un simple comando de curl, el un terminal:

curl http://nov/js/js.php -o jsfull.minified.js

Ahora nuestra plantilla Smarty pasa a ser:

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title></title>
    </head>
    <body>
        Hello World
{if $dev}
        <script src="js1.js" type="text/javascript"></script>
        <script src="js2.js" type="text/javascript"></script>
        <script src="xxx/js1.js" type="text/javascript"></script>
{else}
        <script src="jsfull.minified.js" type="text/javascript"></script>
{/if}
    </body>
</html>

Además de esto es recomendable poner un prefijo a nuestro archivo js para que el nombre sea distinto y asegurarnos así que en navegador renueva la caché y se descarga la última versión.

<script src="jsfull.minified.20110216.js" type="text/javascript"></script>

Conclusión final

Y esto es todo. Como más de uno se habrá dado cuenta esta misma técnica podremos emplearla también para unir archivos CSS y reducir así los tiempos de carga de este tipo de archivos.

¡Nos vemos en la próxima publicación!

¿Necesitas desarrollar un proyecto web o para móviles? ¡Estamos disponibles!

Visitar Cokidoo

Cokidoo, los creadores de Ontuts, desarrollamos proyectos tecnológicos centrados en redes sociales y aplicaciones web, aplicaciones móviles y consultoría web y bases de datos.

Somos jóvenes, inquietos, versátiles, apasionados por la innovación y enfocados en las nuevas tecnologías. Con Ontuts tratamos de compartir nuestro conocimiento adquirido en los distintos proyectos, ayudando a la comunidad y mostrando nuestra capacidad tecnológica.

Si necesitas un presupuesto sin compromiso, estamos disponibles, no dudes en contactar con nosotros.

Comentarios en esta publicación (17 comentarios)

¿Te ha gustado esta publicación? ¡Puedes compartir tu opinión con todos nosotros! Simplemente pincha aquí mismo.

Tutorial light pero muy muy interesante para gente que todavía tiene que escoger entre facilidad de desarrollo y optimización de archivos estáticos.

¡Gracias por el aporte Gonzalo!

Gracias por el artículo.

Una duda. Simplemente con el comando curl cargamos un archivo cualquiera que sea en segundo término?

El programa curl, al igual que wget, es un cliente HTTP (bueno también FTP), que nos permite automatizar tareas. Para generar el archivo js estático podríamos abrir el navegador, poner la url y guardar el archivo js generado. Pero hacer esto una y otra vez, cada vez que pasamos un archivo a producción es tedioso y repetitivo. Es por esto que podemos automatizar estas tareas usando curl o wget. El comando curl que uso en el ejemplo lo que hace es descargarse la url seleccionada y grabar el resultado en el archivo jsfull.minified.js

pues lo ideal seria guardar un cache, para que la primera vez si sea generado el “allJs-min” que se necesite de forma automática(para no generarlo en cada pase a producción)

se me ocurre esto:
——– allJs.php ——–
<?php


if(!isset($_SESSION["jsCacheado"])){
//aki logica para generar el jsAll-min.js
$_SESSION["jsCacheado"]=true;
}
header("Location:cache/empgsViajes.js");
—————————
aquí se generaría el js en cada session incluso se podría generar uno por usuario o solo se refrescaría cada que un usuario iniciara una session

ó bien se me ocurre algo mas practico:

————- AllJsV2.php———–
<?php
if(!file_exists("cache/allJs-min.js")){
//aki logica para generar el allJs-min.js
}
header("Location:cache/allJs-min.js");
—————————————-
aquí tendríamos que eliminar el cache cuando se quisiera re-generar el .js y el nuevo allJs-min se generaría a la siguiente visita y así el único proceso que hacemos es un redirec si es que ya se tiene generado

también una observación IMPORTANTE.. en lo personal creo que NO se debería de generar el allJs-mi.js con un escaneo de carpetas :S ya que eso si te genera el js pero puede que no sea correcto, algunas veces una librería depende de otra y el orden en que se incluye es importante, ya que si un script dependiente de otro es incluido antes de su dependencia el js no funcionaria o funcionaria mal, creo que una mejor forma seria crear una función donde se pasara un array con los archivos a incluir y obviamente en el orden que se incluyen y así ya el desarrollado tendría el control, no seria automático pero se evitarían posibles fallas, igual se puede hacer automático pero los archivos tendrían que ser nombrados de una forma que ordenándolos alfabéticamente por nombre nos diera el orden que se necesita

a lo que me refiero es a lo siguiente :

- algun-pluginjQuery.js
- jQuery.js

el primero depende de que jQuery exista, ya que los plugins modifica el Prototype de jQuery si no existe al momento de que se intente modificar obviamente el script truena… si el "ls" del dir primero me da algun-plugin este se incluye antes de jQuery y ps la cosa ya no funcionaria bien :S…..

Primero. Con PHP podemos generar dinámicamente js sin problemas, pero no lo recomiendo como norma general. Me explico. Js es por naturaleza estático. Esto quiere decir que no necesitamos un servidor Web con soporte PHP para servirlo. Si la aplicación es pequeña y tiene poco tráfico, no tiene importancia. Pero si tiene mucho tráfico piensa cada archivo estático que sirvas estas usando una instancia de tu interprete PHP (que no lo necesitas realmente). Eso consume CPU y RAM y esas cosas cuestan dinero (piensa por ejemplo en un hosting en amazon que pagas por uso de CPU). Es por eso que hay que evitar la generación dinámica de contenido estático siempre que sea posible. Esto te permitirá pasar tus archivos js, css, … a otros servidores sin problemas. Mira por ejemplo como lo tienen montado meneame.net o cualquier periódico online. Verás que la parte js, css e imágenes las sirven desde servidores estáticos diferentes que el/los servidores “principales” desde donde generan contenido dinámico. Con los ejemplos que comentas tienes que tener mucho cuidado con las cabeceras que envías relativas al cache (expires y similares) y sobre todo a la interpretación que hacen los navegadores de estas cabeceras (suelen diferentes). Todo lo que pueda ser estático que lo sea. Ahorraremos recursos y dinero (siempre y cuando el tráfico sea elevado)

Por otra parte. Si tu aplicación necesita un orden específico para que el js funcione bien evidentemente no te vale la lectura recursiva del directorio, pero si la combinación de archivos js para minimizar las peticiones HTTP. No estoy diciendo que todo el js deba estar en único archivo. Simplemente unirlos siempre que sea posible (y muchas veces lo es)

Con respecto a jquery, lo que no recomiendo es servirlo todo desde tu server. Siempre que puedas usa el CDN de google o similares. No malgastes tu ancho de banda en algo que te lo dan gratis (y muy bien por cierto). jquery lo puedes cargar desde allí sin problemas. Los plugins posiblemente no pero de todas formas modificar el script para que pase de una búsqueda recursiva a un lista de rutas de archivos js es algo trivial(sustituir el foreach de la línea 26).

jQuery era un ejemplo U.u… comprendo lo del CDN… no hablo concretamente de jQuery, bueno te cambio el ejemplo.. y en un uso real.. trabajo donde son kilos y kilos de JS, ordeno todo con estructura en packeges y cada JSON en un archivo independiente ej

app,js
———————
window.app={}
——————–

app_utils.js
———————
window.app.utils={
unaFunction:function(){}
}
———————

app_cliente.js
——————–
window.app.Cliente{
//prototype(class) Cliente
}
——————–
app_mod_aminClientes.js
———————
window.app.mod.altaCliente={
//muchos métodos aquí para el modulo alta, instancias de clientes bla bla bla….
}
———————

y bueno al final tengo un montón de archivos.. y hay que crear un o varios archivos min’s para producción o bien un .js gigante que contenga toda la estructura de la app (como lo hacen librerías como jQuery, OpenLayers etc) el problema de esto es que seguramente voy a estar descargando código que muy posiblemente no se ocuparán, por eso creo que es importante el orden

en los ejemplos realmente no estas generando nada solo haces una redirección a menos que el js no exista entonces se crea y se redirecciona hacia el siempre se descarga el js el PHP nunca se envía la respuesta al explorador siempre la respuesta es un header que redirecciona al js , ya verifique que el el que siempre se termina descargando es el .js y si este no cambia se descarga con un status “304 Not Modified” (por lo cual no se descarga y se toma del cache de explorador) y si siempre existe el js estas descargando algo estático nunca nada dinámico.. el error si seria generar en cada petición el archivo min para posteriormente descargar pero en eso ya se pensó por eso primero se evalúa la existencia y en base a ello se crea y se redirecciona o solo se redirecciona.. de hecho la idea era meter un control para q en producción no se generara el js en cada petición :S…. (el problema que mencionas en el post)

aunque al final ya no le vi propósito a este post.. postulas una tesis que al final tu mismo la anulas :S…

Sieto discrepar. Bueno la verdad es que no. Me encanta discrepar y el debate :) . Échale un vistazo por ejemplo a la librería jQueryUI. En la opción de descarga te permite construirte tu mismo la librería que necesitas seleccionando los módulos que quieres cargar y los que no. El resultado es un único archivo js. La idea es la misma. Podemos incluir a mano una a una las librerias en nuestro HTML (con diferentes tags). La aplicación va a funcionar igualmente pero el tiempo total de descarga (que vemos el el firebug) va a ser menor con menos peticiones HTTP. Piensa también que cuando el navegador descarga js bloquea las descargas paralelas, por lo que el problema se agraba. Podemos discutir si es funcional o no el uso de la búsqueda recursiva para generar el archivo final (como te digo pasar a una lista manual de rutas js es trivial). Esto depende de tus necesidades. Pero reducir el número de peticiones HTTP para mejorar el tiempo de carga ya no tanto. Otra cosa es que estemos hablando de no incluir todo el js a la vez en el momento cero y cargarlo según lo vayamos necesitando. Entonces estamos hablando de cosas tipo Require.js. Yo suelo usar la librería Dojo y es lo más normal. Esto es la solución cuando trabajamos con librerías js muy grandes (dojo por ejemplo).

Piensa que no es lo mismo descargarte un archivo js de 100k que cuatro de 25k. La idea del artículo es combinar archivos para reducir las peticiones HTTP. El tema de la búsqueda recursiva es un ejemplo. Si te viene bien (a mi me suele venir bien) lo usas y sino no (a mi hay veces que no me viene bien tampoco y pongo una lista de paths, pero generando siempre el mínimo número de js posibles).

Respecto a lo de generar js con PHP. No digo que no se pueda. Pero si el sitio es grande no se lo puede permitir tan a la lijera (sobre todo si lo estamos usando para mejorar el rendimiento). El cliente (navegador) siempre se descarga contenido estático. Bien sea HTML, CSS, JS, … PHP se ejecuta en el servidor. Aunque solo mandes la cabecera 304 (bueno para el navegador), estas ejecutando código PHP en el servidor. Esto implica que apache (o el server que uses) ha creado un proceso PHP en memoria para resolver la petición. Si el archivo es js y no php te puedes evitar esto. Encima te puedes montar un server sin PHP y ahorras CPU e incluso ancho de banda (trafico de subida con las cockies).

jajajaja.. no te voy a mentir.. cuando lei tu comentario creí que alguien estaba criticando al autor luego me di cuentas que eras tu mismo :S ya luego lei nuevamente el articulo y ps sólito te contradices :/…. LOL… si entiendo todo lo que dices y motivos, el objetivo es “clonar” la idea de los sprites de imágenes/ccs en los archivos js y obviamente también puede aplicar los archivos .css(donde también es importante el orden de ello depende el peso de las reglas si se aplican a un mismo elemento)..

al final la recomendación es genera tus archivos min con una herramienta tipo Yslow. así que ya no le veo cabida aqui a PHP :S ni razon de existencia al post mejor se hubieses mostrado la idea y mostrar como hacerlo con Yslow o de gual seria mejor idea pasar el script PHP a un script PHP-CLI ejecutarlo como parte del setup en el pase a producción y mostrarlo como alternativa a las otras herramientas

aunque te diría que en el caso de los ejemplo ya se me hace entrar en una paranoya de la optimizan por que la verdad no creo que ese pequeño proceso sea un problema vital para una gran mayoría de los sitios que nos toque desarrollar, pero que si de paranoya “optimizadora” hablamos igual podríamos funcionar las img(con src a un base64 aunque no jala en IE7 =( ), js y css en un solo archivo .html y así llevarnos 1 sola petición HTTP pero eso creo que ya es mucho ¿no cres :S?

LOL… bueno = fue un placer debatir y discrepar! =)

Domingo Martinez

Saludos, me parece muy interesante tu aporte, una pregunta: donde obtienes el jsmin.php

gracias

Tienes un enlace en el artículo. DE todas formas el enlace es el siguiente: https://github.com/rgrove/jsmin-php/

Oliver

Yo la verdad es que utilizo SmartOptimizer PHP, que viene con dos modos de uso… y es muy interesante en cuanto a la carga, ya que hace varias cosas… Lo primero es que limpia tu code js, o css, después del minimizado aplicado(embebe png y gif en css), lo comprime y lo ultimo cachea el script.

Lo bueno de esto es que solo hace toda la carga la primera vez, luego lo demás es cache y gzip, tambien te da opciones de incrustar varios css o jss juntos como lo haces tú. ventajas de poder seguir trabajando en los independientes con sus comentarios internos, sin preocuparnos de compilar, el mismo detecta los cambios y recachea..

De lo más interesante con apenas carga de servidor, con un .htacces o directo, según configuraciones personales.

Simple de configurar… aquí está el link: http://farhadi.ir/works/smartoptimizer

Un saludo

Lorena Leiva Román

Una pregunta muy simple Gonzalo ¿hay algún página web o algún estudio que indique cuánto es el tiempo promedio que una persona quiere esperar por una descraga en la web? Me imagino que deb haber estudios sobre esto.

Saludos cordiales, Lorena

Jr

Muy buenas, tengo una duda existencial, espero me puedan ayudar, me gustaría poder juntar todos los archivos .js de jquery que al cargar la página, ésta tarde menos, pero copiando y pegando el contenido de los archivos en uno, no funciona, saben algo de esto? Gracias, por su atención

Hola
Muy buen material, muchas gracias..

Slds..

Hola,
normalmente estaria contigo pero te pongo un elemplo. Si tenemos un js con todo lo necesario para toda la web que pesa por ejemplo 30kb con una conexion wifi o red no habria problemas pero si tienes el diseño en responsive design y quieres ver la web en un smartphone con 3g esos 30kb en mi punto de vista son segundos que el usuario tiene que esperar para ver la web.
Ahora me debatoconmigo mismo en separarlos para reducir el tiempo cuando visita por primera vez el index y si visita otra página que carge sus respectivos js teniendo en cache los genericos para toda la site.
Que opinas?

MASB

Hola,
Gracias por las ideas que das.
Tengo una duda, en mi proyecto queremos unificar los archivos css que utiliza dojo y queria saber si esto puede dar algun error en el funcionamiento de la aplicacion.

Gracias y un saludo.

Con Dojo no es recomendable usar esto con los archivos js, especialmente con las últimas versiones que implementan el flamante AMD, De todas formas para los css no tiene que haber ningún problema.