Actualizar a HTTPS y otras mejoras

Actualizando sitio web

Figura
Figura. HTTPS activado en este sitio

Desde hace algunos meses los navegadores vienen advirtiendo al usuario que una página no es segura si no está bajo el protocolo HTTPS. Por ahora se limita a páginas como las que recopilan contraseñas. Pero más adelante marcaran como no seguras todas las páginas que no usen HTTPS. Por otro lado el proveedor donde tengo alojado este sitio ya está ofreciendo HTTPS incluido en el coste del paquete. Así que no hay excusa para no implementar esta importante actualización.

Para incorporar HTTPS fue necesario modificar todos los enlaces de http a https. La mayor parte de los enlaces son relativos, pero algunos usan URL absolutas como los <link rel="canonical" href="..." />.

Así que tendría que modificar el contenido de todos los documentos HTML, por lo que debía aprovechar esta ocasión para incorporar otras importantes mejoras que tenía en mente. En unos apartados más abajo comentaré con detalle las siguientes:

Otra mejora significativa es la sustitución de Sprites CSS por Iconos SVG cuyos detalles ya publiqué en ese tema. En resumen se trata de usar SVG en lugar de imágenes porque ocupan menos, se les puede aplicar estilo CSS fácilmente y se escalan a cualquier tamaño sin perder calidad.

He modificado un poco la estructura de la página, especialmente en lo que se refiere al lateral derecho. He incluido sub-laterales dentro de este lateral que van apareciendo sucesivamente desplazados a la derecha a medida que agrandamos el ancho de la ventana.

Figura
Figura. Cookie para configuración de usuario

Otra mejora implementada es facilitar algo para la configuración de usuario guardando los valores en una cookie. Se trata mejorar la accesibilidad con un botón con el icono que maximiza el ancho de pantalla y otro con el icono que aumentará el tamaño del texto. Por ahora esas configuraciones sólo permiten valores booleanos, es decir, pantalla maximizada o minimizada y texto grande o normal.

En la Figura puede ver el contenido de la cookie config tras pulsar los botones para maximizar pantalla y agrandar texto. La cookie permanecerá activa durante la sesión del navegador, por lo que al pasar de una página a otra se aplicará la configuración.

En el JS general.js podrá consultar la función aplicarConfig() que aplica la configuración de la cookie. Otras funciones para el manejo de la configuración y de las cookies son leerCookies(), leerConfig(), getConfig() y setConfig() que también puede encontrar en ese JS.

Actualizar a HTTPS

Páginas no seguras sin HTTPS
Aviso de página no segura al no usar HTTPSImagen no disponible
Las páginas pueden ser marcadas como no seguras cuando no se use el protocolo HTTPS.

Desde Enero 2017 el navegador Chrome está empezando a marcar las páginas con protocolo HTTP como no seguras, como puede ver en el blog de Google Security titulado Moving towards a more secure web. Otros navegadores como Firefox también lo están haciendo.

En las capturas de pantalla anexas puede ver los mensajes que estaban saliendo en mi sitio antes de aplicar HTTPS. Este marcado de páginas no seguras se está llevando en dos fases. En primer lugar avisarán de aquellas páginas sobre HTTP que recopilen contraseñas, como el ejemplo mostrado en la imagen. Se trata de un ejemplo de uso de un <input type="password">. En la segunda imagen verá que Firefox saca un mensaje explicando que la conexión no es segura para enviar una contraseña por HTTP.

Webmaster de Google también estaba avisando Las páginas no seguras que recopilen contraseñas generarán advertencias en Chrome 56. En un futuro cercano todas las páginas HTTP vendrán con un aviso de Not secure.

Comentaré los pasos que he seguido para hacer esta adaptación. Cambiar de HTTP a HTTPS en el servidor es muy fácil cuando estamos en un alojamiento compartido. El proveedor nos ofrecerá alguna utilidad en el panel de control para hacer el cambio de forma rápida. En mi caso en 1and1 fue muy rápido, cambié el protocolo e inmediatamente ya estaba el certificado disponible y las páginas entraban con HTTPS.

Así que el problema estriba en lo que tenemos que hacer en nuestas páginas. Y más ahora que también voy a realizar muchos cambios en la mayoría de ellas. De hecho tendré que reemplazar la mayor parte de los documentos HTML y algunos JS y CSS para contemplar la otras mejoras mencionadas más arriba. Así que creí conveniente disponer de una página de mantenimiento para evitar mostrar un mal funcionamiento del sitio. En el htaccess principal puse estas directivas:

RewriteEngine on    
RewriteCond %{REQUEST_URI} !^/res/srv/error/mantenimiento\.html$ [NC]
RewriteRule ^ /res/srv/error/mantenimiento.html [L,R=301]

Mientras llevaba a cabo los cambios, todas las peticiones se redigirían a esa página de aviso, página que está totalmente aislada de otros recursos así que cualquier cambio en JS, CSS u otros documentos no le afectarían. En principio el cambio no me llevó mucho tiempo, pero pudiera ser que se presentasen problemas y se alargara más de lo previsto, así que no está de más usar esa redirección temporal.

También es recomendable desactivar los robots de búsqueda para que no rastreen el sitio mientras estamos en mantenimiento. En robots.txt pondremos algo como esto:

User-agent: *
Disallow: /

Cuando todos los cambios se hayan completado y HTTPS esté funcionando reactivaremos el robot, eliminaremos la redirección previa a la página de mantenimiento y agregaremos la siguiente redirección:

RewriteCond %{HTTPS} !=on [OR]
RewriteCond %{HTTP_HOST} !^www\.wextensible\.com$ [NC]
RewriteRule ^(.*)$ https://www.wextensible.com/$1 [L,R=301]

Los RewriteCond son condicionales para ejecutar el siguiente RewriteRule. Cada uno en una línea funcionan con una conjuntiva, es decir, como si hubiera un operador [AND]. En otro caso podemos poner una disyuntiva explícitamente con el operador [OR]. El primer condicional comprueba si la petición no se está haciendo con el protocolo HTTPS. El segundo condicional comprueba si el dominio no es el canónico, que para este sitio lo tenía establecido en www.wextensible.com. En cualquiera de los dos casos redireccionamos a la misma página pero con protocolo HTTPS y con la URL canónica.

Reducir tamaño de los documentos HTML

Figura
Figura. La conexión SSL supone una carga de 152 ms con HTTPS, carga que no existía con HTTP.

El protocolo HTTPS es seguro porque encripta las comunicaciones. Seguirá siendo posible interceptarlas, pero no será tan fácil descifrar su contenido. Pero esto tiene un coste en las conexiones. En la Figura puede ver una captura de pantalla de las conexiones que suceden con una petición a la página principal de este sitio con HTTPS ya activado. La barra de color morado etiquetada con las siglas SSL es la correspondiente a la negociación SSL necesaria para llevar a cabo la conexión HTTPS. Ocupa en esa prueba unos 152 ms, tiempo extra que antes no teníamos con HTTP.

Esto significa que debemos optimizar más que antes nuestros recursos. Los documentos que se reciben en este sitio ya están minimizados y comprimidos con GZIP. Pero aún podemos hacer más cosas para mejorar la velocidad de carga con algunos recursos.

Tal como comenté en el tema sobre la latencia web, los servidores tienen asignada una ventana inicial (IW) de 14600 bytes. Un contenido de ese o menor tamaño puede ser recuperado con un único RTT. No siempre podemos conseguirlo, especialmente con los documentos HTML. Pero hay que intentarlo porque notaremos una apreciable mejora en la velocidad de carga.

Figura
Figura. Reducción de un documento HTML de 27,37 KB a 9,53 KB.

En la Figura puede ver una captura de pantalla del Developer Tools de Firefox. Recorté los datos que me interesan para observar el tamaño del documento Temas en este sitio. Realmente esa imagen es una composición de dos capturas. La parte superior es la de ese documento antes de realizar los cambios que he llevado a cabo. En la parte inferior está el nuevo documento. Antes tenía un tamaño de 27,37 KB (103,62 descomprimidos) y ahora tiene un tamaño de 9,53 KB (34,48 descomprimido).

Figura
Figura. Un mecanismo para seguir descargando contenido

Por supuesto que esa reducción es debido a que hay una verdadera disminución de contenido. En lugar de ofrecer todo el contenido del documento se presentan sólo una parte superior que no supere cierto tamaño. Si el usuario consume ese contenido y llega hasta el final de la página se le ofrecerá algún mecanismo para descargar más contenido. Tal como se observa en la Figura he optado por un simple botón. Pero también cabrían otras opciones como la del scroll infinito. En cualquier caso tendríamos que disponer de ese contenido adicional en el menor tiempo posible.

Para llevarlo a cabo dividimos el contenido en porciones de tamaños similares. Yo utilizo una herramienta propia que realiza este trabajo en porciones de 10 KB. Como se enviará comprimido tendrá un tamaño mucho menor, en torno a los 4 KB incluyendo cabeceras. Las porciones se guardan en un JS con una estructura JSON, estructura de datos adecuada porque cada entrada de la lista de índices de temas mantiene una misma estructura. Por ejemplo, el documento Temas tiene ocho listas. Las dos últimas siempre se presentan con la carga de la página. La primera que podría solicitar el usuario sería la antepenúltima, la lista 6 que contiene este JS:

Wextensible.listaIndex.lista6 = [
    {
        "titulo": "Métodos para crear un nuevo Array en ES6",
        "href": "\/temas\/javascript-array\/metodos-crear.html",
        "key": "javascript",
        "dia": 3,
        "mes": 7,
        "anyo": 2016,
        ...
        "texto": "..."
    },
    ...
];
    

Cada entrada en la lista es un objeto con unas claves que nos permitirán reconstruir el HTML con JavaScript. Aunque no voy a entrar en ello, esto lo puedes ver en general.js, en la función mostrarMasIndex() que ejecuta el evento click sobre el botón Mostrar más entradas.

La lista siguiente a recuperar debería estar disponible de forma inmediata cuando el usuario la solicite. Para ello podemos usar un prefetch que ya viene en el HTML de la página:

<link rel="prefetch"
href="/res/inc/accesos-data/listas-index/temas/lista-6.js"
as="script" />
    

El prefetch recupera un recurso pero el navegador no lo aplica o ejecuta, sino que queda en una especie de cache. Cuando en nuestro caso el usuario pida más entradas, la función mostrarMasIndex() creará un elemento <script> incorporándole esa misma ruta. Al ser insertado en el DOM el navegador recuperará el recurso desde el recibido en el prefetch en lugar de solicitarlo al servidor.

Con esa acción también volvemos a incorporar otro prefetch para tener disponible la siguiente lista, por si el usuario nos sigue pidiendo más contenidos. Y así hasta que se acaben los contenidos, en cuyo caso se desactiva el botón y se comunica que no hay más contenidos.

Por ahora estoy aplicando esta técnica a las páginas que contienen índices de temas: Inicio, Temas, Como se hace, Herramientas y Artículos. Son ideales para esto pues tienen la misma estructura de contenido. Para otras páginas habría que definir la forma en que se va a dividir en porciones el contenido. Y tener en cuenta cosas como referencias a elementos ubicados en porciones que aún no estén presentes.

Reducir tamaño de general.js y prepararlo para ES6

Figura
Figura. Reducción del tamaño de general.js.

Los tamaños que podemos ver en el Developer Tools del navegador Firefox son los del contenido, sin incluir cabeceras de la conexión. En la Figura podemos ver esos tamaños. La columna con el título Transfe... es la del tamaño del contenido recibido (transferido). Realmente es el valor de la cabecera HTTP Content-Length que informa al navegador del tamaño del contenido quitando todas las cabeceras HTTP. El Content-Length siempre se da en Bytes mientras que Firefox lo expone en KB en esa columna. La otra columna Tama... es el tamaño del documento una vez descomprimido, pues esos archivos se reciben comprimidos con GZIP.

El archivo es general.js, un JS que contiene funciones de uso común así como otras para ejecutar en la carga de la página. Antes tenía un tamaño comprimido de 14,48 KB, lo que supone 14,48 × 1024 = 14828 Bytes. Vemos que se pasa de los 14600 que tiene la ventana inicial IW, y aún sin sumarle las cabeceras HTTP.

Entonces el objetivo es rebajar más el tamaño de ese archivo JS. Ya estoy aplicando varias cosas para reducir el tamaño:

FaseKB TamañoKB Reducción
acumulada
% sobre
primer valor
En desarrollo154,310
Minimizado59,02-95,29-61,75%
Refactorizado47,89-106,42-68,97%
Comprimido14,48-139,83-90,62%

En desarrollo los comentarios, espacios innecesarios y tabulaciones ocupan bastante. Con el minimizado los quitamos. El refactorizado de variables orientado a reducir el tamaño también puede ayudar. En mi caso se trata de cambiar los nombres de las variables por una o dos letras para así acortar el código. Por último el comprimido GZIP es el que más consigue reducir el tamaño. A partir de aquí lo que puedo hacer es revisar ese JS y eliminar lo que no se necesite.

Ese era el objetivo, pero otro que me había planteado es cómo hacer un JS que tuviera soporte para la últimas incorporaciones de EcmaScript (ES6 y siguientes versiones) y que el sitio no se rompiera si el navegador no lo soportaba. Hice una copia de general.js y la denominé general-old.js. La primera tendría las innovaciones de ES6 y la segunda quedaría a la altura de un soporte para navegadores como IE8.

Al final del JS general.js puse Wextensible.generalJs = true de tal forma que si había un error al parsear el archivo el valor de generalJs sería falso. En el script al pie de cada página puse algo como esto para detectar si el archivo JS se cargó bien:

var Wextensible = {}, wxG;
Wextensible.iniciarPagina = function() {
    wxG = Wextensible.general;
    wxG.adjudicarEventosGenerales();
    //siguen acciones en la carga de la página
};
window.onload = function() {
    if (Wextensible.generalJs){
        Wextensible.iniciarPagina();
    } else {
        var elemento = document.createElement("script");
        if (elemento.addEventListener){
            elemento.addEventListener("load", Wextensible.iniciarPagina);
        } else if (elemento.readyState == "uninitialized") {
            elemento.attachEvent("onreadystatechange", function(){
                if (elemento.readyState == "loaded") 
                    Wextensible.iniciarPagina();
            });
        }
        elemento.async = true;
        elemento.src = "/res/inc/general-old.js";
        var head = document.getElementsByTagName("head")[0];
        if (head) head.appendChild(elemento);
    }
};//fin onload
    

En caso de error en el parseado del JS cargará la versión general-old.js. Ahora podría empezar a reducir el tamaño del archivo general.js, reduciendo y eliminando funciones que antes eran necesarias sólo para dar soporte a navegadores antiguos.

Figura
Figura. El JS utiles.js contiene parte de general.js

Para reducir más el tamaño de general.js reubiqué algunas funciones de poco uso en otro archivo que llamé utiles.js. Son funciones que no se usan en todas las páginas, como el JS que maneja el Slider de imágenes. O incluso otras como el necesario para Imprimir un trozo de página, función de uso esporádico.

En la Figura puede ver una captura de pantalla de todos los JS que entran en juego con esta página que está viendo. Se observa general.js y tambien utiles.js, pues más arriba existe un slider de imágenes que fuerza la carga de ese archivo.

Veámos el caso de imprimir un trozo de página. Antes existía la función imprimirTrozo (elemento, inHead, vistaPrevia) con todo el código en general.js. Esa función ahora se reubica en utiles.js y en general.js sólo existe esto:

imprimirTrozo: function(e, i, v) {
    wxG.ejecutarUtiles("imprimirTrozo", ...arguments)
},
    

Tengo que seguir manteniendo esas funciones en general.js pues ya se referencian en muchas partes del sitio. Pero ahora la función original con todo su código interior se encontrará en utiles.js, recurso que se carga siempre a petición. La primera vez que se llame a alguna función de las que están en utiles.js procedemos a cargarlo con la función cargarScripts(). Mientras se recibe el JS vamos encolando las funciones solicitadas y sus argumentos. Cuando recibamos el JS procedemos a desencolar las funciones para ejecutarlas. En cualquier ejecución posterior si ya utiles fue cargado sólo resta ejecutar la función. Este es el código de ejecutarUtiles():

colaUtiles: [],
utilesCargado: false,
cargandoUtiles: false,

ejecutarUtiles: function(nombreFuncion, ...args) {
    if (wxG.utiles && wxG.utilesCargado){
        wxG.utiles[nombreFuncion](...args);
    } else {
        wxG.colaUtiles.push([nombreFuncion, ...args]);
        if (!wxG.cargandoUtiles){
            wxG.cargandoUtiles = true;
            wxG.cargarScripts(["/res/inc/utiles.js?mt=" + wxG.version], 0, 
            () => {
                while (wxG.colaUtiles.length > 0){
                    let arr = wxG.colaUtiles.shift();
                    wxG.utiles[arr[0]](...arr.slice(1));
                }
            });
        }
    }
},
    

Con esas medidas logré reducir el tamaño de general.js y al mismo tiempo poder soportar todo lo nuevo de ES6 y siguientes versiones. Los archivos general-old.js y utiles-old.js seguirán estando escritos en ES5 para que sea soportado por navegadores antiguos. No me preocupa el tamaño de esos recursos old, por lo que esos navegadores antiguos tendrán una penalización de coste de conexión, pero al menos seguirán soportando el sitio.