Etiquetas
No sé si habréis notado, pero si accedéis a Menéame desde un móvil o tableta, se carga muy rápido y están disponibles prácticamente todas las funcionalidades de la versión «normal» de la web. Aunque en realidad no se tratan de dos versiones diferentes, es la misma pero con cambios muy pequeños en la lógica del servidor, y muy poco de Javascript.
El 30% de los accesos a Menéame son desde móviles, antes teníamos una versión más reducida (sigue disponible en m.meneame.net), pero recibíamos quejas por ser muy limitado en cuanto a funcionalidades. Por otro lado, era demasiado trabajo mantener dos versiones diferentes.
También queríamos adaptar la web para los monitores Retina y de alta definición que tienen la mayoría de teléfonos y tabletas de gama media y alta (su densidad de pixels por pulgada es mayor -hasta dobla- a los típicos 96 px/pulgada de los monitores tradicionales), para ello hay que detectarlos y servir en estos casos una imagen del doble de resolución.
Así, hace unas semanas nos pusimos a tope para lograr una única versión integrada, con pocas diferencias en la lógica del backend, y casi ninguna modificación hecha con Javascript en el lado cliente. Aunque el PageSpeed no es una medida cualitativa absoluta, sí que da bastante idea de velocidad y ligereza:
Como se puede deducir, las mejoras de velocidad de carga para móviles también afectó positivamente a la versión normal del web. A continuación describiré cuáles fueron los trucos y técnicas más importantes.
Modificaciones y unificación de CSS
La mayoría de los cambios en la apariencia están determinados por el CSS. El CSS original está diseñado para la versión normal de la web, creamos otro CSS sólo para pantallas más pequeñas que modifica algunas de las clases definidos en el principal. Para evitar que el cliente móvil se cargue dos ficheros diferentes (con el sobrecoste de una nueva conexión), usando los templates Haanga unificamos ambos CSS en uno sólo, incluyendo también el CSS de otros plugins (por ejemplo, el de colorbox, que se usa para la visualización de imágenes, galería, votos, etc.).
El software backend de Menéame detecta si es un dispositivo móvil (la variable $globals[‘mobile’]), que se usa para discriminar en algunas de las partes del código, en ese caso se se usa para:
- Si es un móvil, se incluye directamente el contenido del fichero handheld.css.
- Si es un navegador de escritorio o tableta, se lo incluye dentro de una regla @media (max-width: 800px) {…}. De esta forma el navegador adaptará la visualización dependiendo del tamaño de la ventana, o si la tableta está en vertical u horizontal. Podéis probarlo reduciendo el ancho de la ventana del navegador (se pondrá igual que el móvil), o poniendo en horizontal la tableta (mostrará igual que la versión de escritorio si el ancho es mayor o igual a 800 pixels).
En el fichero mnm.php que unifica a ambos, también se envían las cabeceras que indican al navegador que guarde en caché el fichero CSS. En caso de modificarlos, cambiamos el url (la parte de «/v_xx») para forzar la recarga. Esto lo hacemos con una variable de versión en PHP y una regla en el servidor web NGInx para ignorar esa parte:
rewrite /v_\d+/(.+)$ /$1 last;
Cambios en la lógica
Hay muy pocos cambios en la lógica del PHP o de Javascript para la versión móvil o normal. En el PHP se usa la variable $globals[‘mobile’] en unos pocos sitios, y sobre todo para evitar enviar gran cantidad información que no se visualizará en un dispositivo móvil, por ejemplo:
- El contenido de la barra derecha de la versión normal.
- Los avatares de autores de comentarios.
- Cambiar el número de artículos o comentarios que se muestran por página.
- Carga la webfont Roboto sólo para la versión de escritorio (son varios KB, pero los Android ya la tienen, y los dispositivos iOS tienen una Helvética muy similar de alta calidad).
Para todo el resto, la lógica es la misma para cualquier versión.
En Javascript sólo hay un cambio (en la parte var navMenu = new function () {… de main.js). En el móvil están ocultas las cabeceras por el CSS (display:none), salvo la barra negra. En Javascript se coge el contenido del menú de navegación y del formulario de búsquedas
y se lo copia al contenedor del menú superior:
Luego, como vimos que era útil para navegar (incluso en tabletas, con el pulgar izquierdo), el Javascript lo hace para ambas versiones.
Carga de imágenes bajo demanda, y servir imágenes «Retina»
Es importante minimizar la conexión 3G de los móviles, no sólo por el consumo de datos, también por el de la batería (y temperatura que suele alcanzar el chip de radio). Para ello debíamos implementar algo similar al LazyLoad, pero éste es muy grande y «pesado» para móviles. Busqué algo más simple y eficiente. Así encontré el Unveil, éste es suficientemente pequeño para incluirlo en nuestro propio Javascript, pero todavía tenía unos problemas de eficiencia, principalmente en que el código se ejecutaba por cada movimiento del scroll. Esto es un problema de eficiencia, aún más grave para móviles. Para ello programé otro mini plugin de Jquery que implementa el evento «scrollstop» que se dispara sólo una vez después de que el usuario haya dejado de hacer scroll. El código es breve y sencillo (está en main.js):
/* scrollstop plugin for jquery +1.9 */ (function(){ var latency = 75; var handler; $.event.special.scrollstop = { setup: function() { var timer; handler = function(evt) { var _self = this, _args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout( function(){ timer = null; evt.type = 'scrollstop'; $(_self).trigger(evt, [_args]); }, latency); }; $(this).on('scroll', handler); }, teardown: function() { $(this).off('scroll', handler); } }; })(jQuery);
La versión modificada de unveil (en (function($) { $.fn.unveil = function(options, callback) {… } de main.js) ahora usa este evento, y además sirve las imágenes de alta resolución si se trata de monitores de alta resolución.
Para ello, originalmente en el src de las imágenes se pone sólo la de una de un pixel (src=»http://mnmstatic.net/v_4/img/g.gif«, se carga una única vez para todas), los datos para la carga de las imágenes que toca van como «data» HTML5, por ejemplo:
<img data-src="/cache/05/cc/379915-1369303151-20.jpg" data-2x="s:-20.:-40." alt="" src="http://mnmstatic.net/v_4/img/g.gif" class="tooltip u:379915 avatar lazy"/>
El campo «data-src» indica al unveil que esa es la imagen (de resolución normal) que debe cargar cuando sea visible. El campo data-2x indica cuál es la que debe cargar para pantallas de alta resolución. Aquí se indica sólo una regla [s:-20.:-40.]. Es una extensión que programé al unveil, en vez de especificar el URL alternativo (más trabajo para el servidor, y más texto que enviar), sólo se le indica qué debe reemplazar en el URL normal para coger la de alta resolución. En este caso significa cambiar la subcadena «-20.» por el «-40.», por lo que quedará como /cache/05/cc/379915-1369303151-40.jpg. Esta regla es para el tamaño de los avatares de usuarios, para las imágenes de miniaturas de noticias es similar:
<img data-2x='s:thumb:thumb_2x:' data-src='cache/1f/6c/thumb-2059491.jpg' src="http://mnmstatic.net/v_4/img/g.gif" alt='' class='thumbnail lazy'/>
En ese caso la regla es remplazar la cadena «thumb» por «thumb_2x».
Ooops
¿Cómo y cuando se generan las imágenes de mayor resolución para Retina? Aunque desde hace unos días el software ya genera las miniaturas en las dos resoluciones, había que resolver el problema para los artículos anteriores que no tienen la imagen generado. Ya teníamos el programa ooops.php que se llama cada vez que el servidor no encuentra una imagen, analiza el URL, encuentra la original (o la baja del backup en S3 si no está en el disco local) y genera la miniatura en el tamaño solicitado. Sólo tuvimos que modificarlo para que acepte además el «parámetro 2x», por ejemplo:
case "thumb": case "thumb_2x": // La nueva regla case "thumb_medium": // Links' thumbnails $base = $parts[0]; if (count($parts) == 2 && $parts[1] > 0) { $link = Link::from_db($parts[1]); if ($link && ($pathname = $link->try_thumb($base))) { header("HTTP/1.0 200 OK"); header('Content-Type: image/jpeg'); readfile($pathname); $globals['access_log'] = false; die; } } $errn = 404; break;
Imágenes de fondo en el CSS, sprites y las dos resoluciones
Otra de las cosas importantes que hay que hacer es servir las imágenes de fondo especificadas en el CSS para ambas resoluciones. También incluye modificar el código HTML para que las imágenes estáticas (iconos y logos) incluidas como <img> sean imagen de fondo de un <div>. De esta forma se puede controlar en el CSS de que se cargue la resolución adecuada según la pantalla.
Para minimizar las conexiones y tiempo necesario para bajar los logos e iconos comunes a la versión web y móvil creé un par de imágenes donde incluyen a todas ellas (sprites). Por ejemplo los iconos comunes los incluí en un único SVG (con el Inkscape) llamado icons-common.svg. A partir de este SVG se genera la imagen PNG para la resolución normal (icons-common.png) de 20×300, y otro con el doble de resolución, 40×600 (icons-common-2x.png).
.comment-meta .vote.up { ... width: 16px; height: 18px; background: url(.../img/common/icons-common.png) no-repeat 0 -20px; background-size: 20px 300px; } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .comment-meta .vote.up { background-image:url(../img/common/icons-common-2x.png); } }
(Fijaros que el tamaño especificado en background-size es siempre de la imagen completa en la resolución «normal». No servimos el SVG directamente porque versiones antiguas de IE no lo entienden, y porque hay que filtrarlos bien después de gaurdarlos con el Inkscape, éste agrega extensiones y el XML no es nada óptimo.)
Retrasar la carga de Javascript
Esta fue una de las partes más interesantes. Se recomienda cargar los javascripts al final del documento HTML. Así, mientras se cargan e interpretan el navegador ya puede mostrar el HTML descargado hasta ese momento.
En nuestro caso era crucial, las jQuery tienen casi 90 KB de tamaño (unos 30 KB comprimidas), y el javascript propio de Menéame casi 50 KB (17 KB comprimidas). En un ordenador de escritorio no se nota, pero en un móvil, para una persona que accede la primera vez y no las tiene en cache, puede tomar varios segundos, con gran probabilidad de que abandone, o que piense que siempre es así y no regrese.
El problema que teníamos que solucionar: en varias páginas de Menéame se llamaban funciones propias y de jQuery, por lo que requería que éstas ya estuviesen cargadas. El truco fue retrasar estas llamadas, gracias a las funciones anónimas y clausura de Javascript fue bastante sencillo.
Junto con cada página de Menéame se genera «inline» un Javascript de inicialización muy pequeño:
var base_key="{{globals.security_key}}", link_id = {% if globals.link_id %}{{ globals.link_id }}{% else %}0{% endif %}, user_id={{ current_user.user_id }}, user_login='{{ current_user.user_login }}'; var onDocumentLoad = []; function addPostCode(code) { onDocumentLoad.push(code); }
En el array onDocumentLoad se almacenan las funciones que deben ejecutarse una vez que se cargaron jQuery, las funciones de Menéame y el DOM ya esté completo en el navegador. Para ello, el cargarse todo el DOM se ejecuta lo siguiente (en main.js, que se carga al final):
for (var i= 0; i < onDocumentLoad.length; i++) { if (typeof onDocumentLoad[i] == "function") { onDocumentLoad[i](); } }
Dentro de las páginas que necesitan ejecutar Javascript, en vez de llamar a la función original de jQuery, simplemente se hace algo como:
<script type="text/javascript"> var do_change = function() { ... }; addPostCode(function() { $("#w").change(do_change); do_change(); });
(Ni siquiera hace falta definir la función addPostCode, se puede hacer directamente un push de la función anónima, pero así tengo más controlado y puedo cambiar el comportamiento fácilmente en un único sitio.)
Bonus: retrasar la carga de código de Google AdSense
Si usáis AdSense en vustro sitio, el código que os da Google es algo así como:
<script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" async=""></script> <ins class="adsbygoogle" style="display: inline-block; width: 300px; height: 250px;" data-ad-client="ca-pub-xxxxxxxx" data-ad-slot="xxxxxxx"></ins> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script>
Por cada anuncio carga y obliga a interpretar el fichero adsbygoogle.js. Si estáis retrasando la carga de todo el Javascript, esto «molesta» porque empieza a cargar antes. Pero podéis quitar esa línea de cada anuncio y ponerla una sola vez al final del documento, justo antes de cerrar el </body> y después de cargar vuestros ficheros Javascript. Funciona perfectamente, así están en la versión móvil del Menéame (en la versión web usamos el DFP de Google, que es más complicado y trasto).
Fin
Esto no es todo lo que hace falta ni lo que hicimos, pero sí lo principal o que más tiempo me hizo pensar y experimentar. Lo demás suele ser lo habitual para servir de forma eficiente: comprimir el HTML/CSS/Javascripot en el servidor con el módulo gzip, poner fechas de caducidad y caché adecuadas, incluir cabeceras «Last-Modified», etc.
También tuvimos que usar otros trucos para mejorar la usabilidad en algunos sitios con demasiada información, como hacer invisible algunas div en el móvil y mostrar un pequeño icono que al hacer clic muestra la información «oculta», por ejemplo la de los relojitos para la hora de los comentarios:
Pero esto ya son detalles, lo principal creo que os he contado. Espero que os sirva y no perdáis tanto tiempo como yo haciendo pruebas y leyendo millones de artículos 😉
PS: Nos preguntan siempre ¿y no sacaréis una app móvil? No, al menos a corto plazo. Es muy caro, hay que desarrollar y mantener al menos tres versiones diferentes (Android, iOS, Windows, y quizás hasta Firefox OS), por eso estamos esforzando en una versión muy ligera y rápida.
¡Gracias por compartir todo vuestro trabajo! No sólo el código, sino cosas como estas 🙂
Opera Mini -Android- no soporta bien el nuevo cambio. Si quieres te envío una captura.
Ricardo, una vez más kudos por el modo en que enfocas tu trabajo con software, por liberar, por ser didáctico.
¿Estos cambios son nuevos? La última vez que entré a Meneame móvil (hará 3-4 días) no me fijé en algunas de estas cosas que comentas y no sé si son nuevas o ya estaban…
yo echo en falta poder chequear las candidatas desde el movil
@javierchiclana No preguntes, pásalas 😉 (pero puedo probar en mi Android, también)
@Jose Alcántara
Sí, ya estaban, comenzamos hace varias semanas, todo lo que comento aquí está en marcha desde hace más de una semana, el más jóven.
@Don gato
Menú -> pendientes -> candidatas (en la parte superior)
En tu correo la tienes. 🙂
Muchas gracias por compartirlo Ricardo.
Enhorabuena por la nueva version responsive Ricardo. Respecto a la sugerencia sobre el código de Adsense, al generar la etiqueta puedes elegir que sea de tipo asíncronico y esto en principio debería evitar el retraso en la carga del Javascript (https://support.google.com/adsense/answer/3221666?hl=es&ref_topic=1307438)
Saludos!
Nacho
@nachomereneo
El ejemplo que puse es «async» (es el que usamos), pero esto significa que se puede ejecutar en cualquier momento, pero no que se obligatoriamente se retrasa la carga del javascript (y su correspondiente interpretación), ni que se ejecute después de ejecutarse lo demás. Sólo que el navegador puede ejecutarlo más tarde.
Esto implica que el javascript de AdSense se carga a interpreta antes que el del sitio (si tiene sus javascripts al final de todo), e inclusive puede ejecutarse antes. Al llevar la carga al final, haces que primero se cargue lo «local» y luego el de AdSense. No produce otros efectos (el javascript de AdSense recorre todos los «ins» que hay en el documento, y marca los que ya ha pasado con un «data-done»).
Yo recomendaría esta solución para todos los sitios móviles 😉
Pingback: Técnicas y trucos para la versión móvil “responsive” del Menéame | Buy Smartphone and Tablet Firefox OS
Wow, muchas gracias. Vas a ahorrar mucho tiempo de investigación a muchos como yo.
¿Puede ser que las imagenes del sitio web tarden un poco más en cargar, así como la fecha de publicación de las noticias? Sobretodo lo noto cuando busco una noticia antigua en el buscador…
Puede ser, si no hay copia del tamaño pedido en el disco y hay que bajar el original desde S3 y escalarlo.
Buen trabajo!
Lo de los iconos yo lo haría de otra forma: con los SVG haría una tipografía especial para Menéame usando la webapp de Icomoon (http://icomoon.io/app/)
Luego los insertaría usando pseudo-elementos :before y :after (como hace Bootstrap), y para versiones viejas de IE usaría un polyfill como jQuery Pseudo (http://jquery.lukelutman.com/plugins/pseudo/) al que metería detectando con Modernizr.
Así, al ser tipografía, podrías escalar los íconos al tamaño que hiciera falta y se verían siempre perfectos en pantallas de alta densidad (y no necesita preprocesar imágenes como en tu solución).
Saludos.
Pingback: Técnicas y trucos para la versión móvil “responsive” del Menéame
Pues no sé si está relacionado con estas optimizaciones, pero el otro día me desesperaba intentando ver Meneame en el móvil con la opción de «Versión de escritorio». Quería verlo como en el PC y no había manera en ningún navegador, de alguna manera la página detectaba que era un móvil. Total que al final molesto me tuve que levantar del sofá para poder ver Meneame como quería, exactamente igual que en el PC.
Una cosilla, has probado a poner el atributo defer en los javascript que cargas? (tanto en el jquery.min.js como en el main.js)
Porque aunque hayas retrasado la carga del js poniéndolo al final del html, cuando llega a éste se paraliza el render del DOM del HTML, la carga de imágenes, etc…
Cuando le pones ese atributo (o el async) le estás confirmando al navegador que no se va a encontrar con ningún document.write y que por tanto puede seguir con lo suyo.
Me he fijado que, en Desktop y con 10MB de conexión, el DomContentLoaded no se completa hasta los 1,5 seg.y el resto de imágenes empieza a cargarse 0.5 seg. después de éste, produciéndose un parón bastante grande a partir de cargar los archivos javascript.
Sé que lo ideal sería cargar todo el javascript cuando se dispare el evento onload pero eso sería en un mundo ideal donde no necesitásemos javascript :p jejeje
Pero vamos, con el defer podrías adelantar el DomContentLoaded (o DomReady) unos 0.8 seg. y la carga del resto de imágenes unos 1.3 seg
Y lo del array onDocumentLoad me parece una gran solución! 😉
Un saludo!
me apunto lo de Adsense. Voy a probar a poner una única llamada al final de la web y los banners sin ellas, gracias.
Si te sirve, para reducir unos kb el peso del archivo css yo utilizo esta tool en firefox http://goo.gl/cZF1Er . Le puedes decir cual es tu sitemap y te revisa todo el sitio devolviendo un reporte con los selectores que no estas utilizando en ninguna parte de la web.
También puedes programar una copia cada x tiempo del archivo ga.js en tu servidor y entregarlo desde el en lugar de desde una IP externa ahorrando una consulta a las DNS
Saludos!
Hola Ricardo, ¿Hay alguna forma de ver Menéame como la versión de escritorio desde un móvil con suficiente resolución (nexus 4, por ejemplo)? Creo que es muy cómodo ver los mejores comentarios de un vistazo en la página principal, aún teniendo que ir haciendo zoom. Lo mismo para acceder a ‘pendientes’ con un sólo clic en vez de tener que abrir el menú de la esquina superior derecha. ¿Es posible ‘engañar’ a la web de alguna manera?
¡Gracias!