Des webapps suivant le modèle Android ICS

En parallèle de la mise en place d’une maquette de webapp pour mes projets personnels, j’ai récemement développé un début de maquette pour permettre de développer des webapps à la manière Android ICS (4.0 – Ice Cream Sandwich).

A l’heure actuelle, la maquette ne propose qu’un nombre limité de fonctionnalités à savoir :

  • Header android ICS Bleu
  • Menu latéral
  • Options de l’application
  • Blocs à la Google Now

L’objectif étant à terme d’arriver à proposer une maquette beaucoup plus complète, avec notamment

  • Indicateur de chargement Fait
  • Header dans les autres couleurs ICS Fait
  • Onglets Fait
  • Gestion des formulaires
  • Toute suggestion pertinente qui sera proposée.

Le projet est partagé en open source sur Github, n’hésitez pas à ajouter votre pierre à l’édifice et récupérer les sources pour vos propres développements !

Pour un premier aperçu des fonctionnalités, voici quelques captures d’écran :
Webapp Android - page par défaut

Webapp Android - menu latéral

Webapp Android - options

Edit 12/07/2013 : La démonstration online est maintenant disponible ici.

Optimisation des images, comprendre la “dirty transparency”

Les consignes webperf un peu partout vous recommanderont de réduire la taille de vos images. Pour cela, il suffit d’utiliser des logiciels appropriés afin de supprimer toutes les méta-données qui pourrait alourdir le composant graphique.

Lorsque l’on travaille sur une image de type PNG-24, avec de la semi-transparence, on se retrouve confronté à un gros surplus d’informations causés par la transparence, appelé “dirty transparency” par les anglophones.

Avant d’expliquer ce dont il s’agit, il faut revoir la structure du format PNG :
Le PNG-24 est composé de trois informations de couleur : Rouge-Vert-Bleu, avec chacune de ces informations codé sur… 24/3 = 8 bits, soit 256 (28) teintes différentes.
En fait, le fonctionnement est identique aux couleurs hexadécimales que l’on utilise en CSS par exemple, mais répété pour chaque point.

Premier point : R-255, V-255, B-255
Deuxième point : R-253, V-254, B-255
Troisième point : R-251, V-253, B-255
etc.

Une fois l’image traitée dans sa totalité, le PNG est compressé afin d’obtenir une image de plus petite taille.

Jusque là, la seule optimisation possible de la taille de l’image résulte de l’utilisation de différentes options pour que la compression des données brutes soit optimale.

Lorsque l’on induit la transparence, On ajoute en une information de plus : le niveau de transparence, sur 8 bits soit 256 (28) niveaux de transparence.

Premier point : R-255, V-255, B-255, A-30
Deuxième point : R-253, V-254, B-255, A-0
Troisième point : R-251, V-253, B-255, A-0
etc.

Quelle est la différence ? Seulement le canal “Alpha” qui donne l’information du niveau de transparence pour le pixel.
L’image continue d’exister avec toutes les informations de couleur, même si le pixel est déclaré “totalement transparent”, et, si toutes ces informations sont différentes comme dans l’exemple montré ci-dessous, la compression ne sera jamais optimale.

Heureusement, ce problème du format PNG est facile à contourner : Il suffit de remplacer tout les pixels transparents (Soit alpha = 0) par une même couleur, totalement transparente :

Premier point : R-255, V-255, B-255, A-30
Deuxième point : R-0, V-0, B-0, A-0
Troisième point : R-0, V-0, B-0, A-0
etc.

Ainsi, la compression de l’image sera bien meilleure, car le pattern le pattern “pixel transparent” est le même dans toute l’image. J’ai personnellement réussi à diviser la taille de l’image jusqu’à 3.

Pour arriver à ceci, j’utilise le code php suivant, que je vous recommande d’utiliser une seule fois, à la mise en production par exemple, puisqu’il est assez coûteux en ressources :

<?php
  1.  
  2. $i = '/path/to/png/file';
  3. $o = '/path/to/output/file';
  4.  
  5. $in = imagecreatefrompng($i);
  6.  
  7. imagealphablending($in, false);
  8. imagesavealpha($in, true);
  9.  
  10. $x = imagesx($in);
  11. $y = imagesy($in);
  12.  
  13. $mx = 0;
  14. while ($mx &lt; $x) {
  15.     $my = 0;
  16.     while ($my &lt; $y) {
  17.         $c = imagecolorat($in, $mx, $my);
  18.         if (($c &amp; 0x7F000000) == 0x7F000000) {
  19.             imagesetpixel($in, $mx, $my, 0x7F000000);
  20.         }
  21.         $my++;
  22.     }
  23.     $mx++;
  24. }
  25.  
  26. imagepng($in, $o, 9);

Une fois le script passé, vous pouvez/devez aussi passer l’image dans votre compresseur classique afin de gagner encore quelques octets sur la taille de vos images.

Quick Tip – Déplacer un commit de l’historique GIT sur une nouvelle branche

Il m’est arrivé récemment de vouloir déplacer un commit de mon historique sur une autre branche : à la base, il ne s’agissait que d’un petit correctif, qui s’est transformé en une fonctionnalité à part.

J’ai en revanche passé beaucoup de temps à trouver comment faire pour effectuer cette modification dans mon dépot.
Vous trouverez ci-dessous la liste de commandes que j’ai utilisé afin d’arriver à mes fins.

Attention, si vous avez déjà poussé les modifications sur un autre dépôt, vous risquez d’avoir des problèmes.
J’utilise par ailleurs le raccourci “git lg” que vous pourrez retrouver dans le dépot dotfiles de mon github.

[09:53] bruno@PC-Bruno:~/git/project[master]$ git lg
* 034bcdf – (HEAD, master) Update on new functionality (Bruno Sabot 10 minutes ago)
* d39bc5b – New functionality (Bruno Sabot 10 minutes ago)
* 4102855 – Small bugfix (Bruno Sabot 10 minutes ago)
* 768cd9a – Initial commit (Bruno Sabot 10 minutes ago)
[09:55] bruno@PC-Bruno:~/git/project[master]$ git checkout 4102855
Note: checking out ‘4102855’.
You are in ‘detached HEAD’ state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at 4102855… Small bugfix
[09:56] bruno@PC-Bruno:~/git/project[(4102855…)]$ git branch project
[09:56] bruno@PC-Bruno:~/git/project[(4102855…)]$ git checkout master
Previous HEAD position was 4102855… Small bugfix
Switched to branch ‘master’
[09:56] bruno@PC-Bruno:~/git/project[master]$ git lg
* 034bcdf – (HEAD, master) Update on new functionality (Bruno Sabot 12 minutes ago)
* d39bc5b – New functionality (Bruno Sabot 12 minutes ago)
* 4102855 – (project) Small bugfix (Bruno Sabot 12 minutes ago)
* 768cd9a – Initial commit (Bruno Sabot 13 minutes ago)
[09:56] bruno@PC-Bruno:~/git/project[master]$ git rebase –onto 768cd9a 4102855 master
First, rewinding head to replay your work on top of it…
Applying: New functionality
Applying: Update on new functionality
[09:56] bruno@PC-Bruno:~/git/project[master]$ git lg
* 1aa8de7 – (HEAD, master) Update on new functionality (Bruno Sabot 4 seconds ago)
* 4afb8d3 – New functionality (Bruno Sabot 4 seconds ago)
* 768cd9a – Initial commit (Bruno Sabot 13 minutes ago)
[09:56] bruno@PC-Bruno:~/git/project[master]$ git checkout project
Switched to branch ‘project’
[09:56] bruno@PC-Bruno:~/git/project[project]$ git lg
* 4102855 – (HEAD, project) Small bugfix (Bruno Sabot 13 minutes ago)
* 768cd9a – Initial commit (Bruno Sabot 13 minutes ago)
[09:56] bruno@PC-Bruno:~/git/project[project]$ git checkout master
Switched to branch ‘master’
[09:56] bruno@PC-Bruno:~/git/project[master]$ git merge project
Merge made by recursive.
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 project2
[09:56] bruno@PC-Bruno:~/git/project[master]$ git lg
* 66b91dd – (HEAD, master) Merge branch ‘project’ (Bruno Sabot 1 seconds ago)
|\
| * 4102855 – (project) Small bugfix (Bruno Sabot 13 minutes ago)
* | 1aa8de7 – Update on new functionality (Bruno Sabot 25 seconds ago)
* | 4afb8d3 – New functionality (Bruno Sabot 25 seconds ago)
|/
* 768cd9a – Initial commit (Bruno Sabot 13 minutes ago)

 

Toute la magie s’opère à la commande “git rebase –onto 768cd9a 4102855 master” :
– Le rebase signifie que l’on veut déplacer des commits de notre arbre
–onto 768cd9a indique que les commits déplacés le seront à la suite du commit #768cd9a
4102855 master sélectionne tous les commits entre 4102855 (exclu) et master (inclus)

A vous de jouer !

Comprendre la compression avancée des CSS

Minifier son CSS a bien entendu pour avantage de réduire le nombre de caractères présents dans une feuille de style, et donc son poids.

Il existe plusieurs types de compression. La plus simple consiste à supprimer les caractères superflus du CSS. Par exemple :

  • Suppression des blancs multiples
  • Suppression des retour à la ligne
  • Replacement des 0px par 0
  • Suppression des ” ou ‘ dans une URL (ex. background:url(toto.jpg))

Il est néanmoins possible d’aller plus loin dans nos optimisations. En quoi ça consiste ?

Regrouper les mêmes règles

h1 { font-size:2em; }
  1. h2 { font-size:1.5em; }
  2. h1 { color: #369; }

Ici, on peut simplifier le code (de 9 octets) en

h1 { font-size:2em; color: #369; }
  1. h2 { font-size:1.5em; }

Sans compter les répétitions éventuelles de certains attributs

Réécrire les règles CSS

h1 { margin-top:10px; margin-bottom:20px }
  1. Dans cet exemple, regrouper les attributs peut faire gagner 17 octets :
  2. &lt;pre lang="css"&gt;h1 { margin:10px 0 20px }

De même que pour le moyen précédent, les répétitions de certains attributs peuvent encore plus diminuer la taille de la feuille de style.

Décomposer les CSS

h1 { font-size:2em; color: #369; }
  1. h2 { font-size:1.5em; color: #369; }
  2. h3 { font-size:1.2em; color: #369; }

En décomposant les règles on gagne 7 octets :

h1 { font-size:2em; }
  1. h2 { font-size:1.5em; }
  2. h3 { font-size:1.2em; }
  3. h1, h2, h3 { color: #369; }

Certes le gain est moins important, mais reporté sur une feuille de style complète, il devient intéressant

Que faire ?

Effectuer ces modifications peuvent casser la mise en page du site, du fait de la gestion des priorités de CSS. Il faut donc savoir le faire avec précaution.

Il existe bien entendu des outils pour gérer ce genre de problématique de compression :

Clean CSS : http://www.cleancss.com/
CSS Compressor : http://www.codenothing.com/css-compressor/

Ces deux outils permettent de faire des optimisations très avancées, tout en étant capable de gérer des niveaux plus où moins élevés de compression.

Quick Tip – Hauteur d’un block de texte

Piqûre de rappel pour certains, découverte pour d’autres, vous avez peut-être déjà remarqué que dans ce cas :

  1. <div style="font-size: 20px">Hello world!</div>

La hauteur du block div n’est pas de 20 pixel, mais “plus”. Ceci est, dans des cas d’intégration assez courants (positionnements relatifs) la source de problèmes.

Il est néanmoins assez difficile de définir ce “plus”, qui semble être variable en fonction de la police utilisée et du navigateur.

Par exemple sous Chrome et Firefox avec Arial :

font-size 30 29 28 27 26 25 24 23 22 21 20
height 36 35 34 33 31 31 30 28 27 25 25
font-size 19 18 17 16 15 14 13 12 11 10 9
height 23 22 20 20 18 16 16 15 14 12 11

Le ratio est donc un calcul approximatif, probablement avec des arrondis, pas évident de connaitre donc la bonne hauteur du bloc.

C’est là que l’on fait intervenir le line-height. Il suffit de le rajouter Pour résoudre le problème :

  1. <div style="font-size: 20px; line-height: 1em;">Hello world!</div>

A présent, le div a bien une hauteur de 20 pixels de haut !

Quick Tip – ORDER BY avec une jointure

Le saviez-vous ?

Le choix des champs pour l’ordre de tri est important dans une requête SQL.
Prennez par exemple la requête suivante :

  1. SELECT *
  2. FROM `ma_table_a` a
  3. INNER JOIN `ma_table_b` b ON (a.`aid` = b.`aid`)
  4. ORDER BY b.`aid` DESC

En plaçant un index sur le champ b.`aid`, celui-ci sera utilisé dans la requête, et celle-ci sera très rapide.
En réalité, ce n’est pas le cas. L’utilisation de cet index n’est absolument pas bon et vous allez probablement autant de temps à executer la requête que s’il n’y en avait pas.

Pourquoi ?

Dans ce genre de requête, vous avez le tri qui est executé à la fin de la requête, juste avant le renvoi des données.
Le tri est executé sur une table temporaire qui ne contient pas d’index, et donc pas celui de la table `ma_table_b`.

Comment corriger le problème ?

Si l’on choisit un index de la première table `ma_table_a`, celle de la close FROM, le tri sera effectué avant la jointure, et l’on peut ainsi bénéficier totalement de l’index de la table placé sur `aid` :

  1. SELECT *
  2. FROM `ma_table_a` a
  3. INNER JOIN `ma_table_b` b ON (a.`aid` = b.`aid`)
  4. ORDER BY a.`aid` DESC

Sur ce problème, en changeant la close ORDER BY par celle de la première table, je passe d’une requête de plus de 5 secondes… à 10ms.

Moteur utilisé : MyISAM
Nombre d’enregistrements : 130 000 / table

Quick Tip – Optimisation simple de boucles JavaScript

N’étant pas resté au bout de la soirée webperf du 21 avril, je ne me risquerais pas à faire un compte rendu qui risquerait d’être incomplet.

Néanmoins, lors de cette soirée nous avons découvert un morceau de code assez surprenant.

Pour se mettre dans le contexte, nous étions en train d’analyser le site de 20 minutes, et
Vincent Voyer
à décidé de lancer un dynatrace.

En regardant les résutlats, nous avons remarqué qu’un script avait un temps d’execution de… 1.1s, ce qui est bien entendu énorme ! Ce script vennait du tag xiti. Celui-ci sera peut-être corrigé, mais il pourra servir de cas d’exemple sur une mauvaise pratique.

En fouillant dans le code on découvre (en réindenté) le code suivant :

  1. var elts = target.getElementsByTagName('*');
  2. for (var i = 0; i &lt; elts.length; i++) {
  3. }

Pourquoi est-ce un problème ? Avant de commencer à expliquer le problème, il est important de connaitre un point important. Pour celui-là, j’ai utilisé statsy.

JS in HTML attributes: 1351 bytes
CSS in HTML attributes: 2790 bytes
JS in SCRIPT tag: 4111 bytes
CSS in STYLE tag: 69039 bytes
All innerHTML: 215207 bytes
# SCRIPT tag: 54
# STYLE tag: 1
# DOM elements: 2143
Cookie size: 340 bytes

Dans la liste des résultats ci-dessus, on remarque que la page comporte 2143 élements. Le problème est simple :
Le getElementsByTagName récupère tout les éléments DOM. Ce qui demande beaucoup de traitement sur une page kilométrique comme celle d’un site de news.

Second problème, la boucle effectuée sur ces éléments recalcule cette selection à chaque itération de la boucle. Les élements sont donc recalculés 2143 fois. C’est simplement énorme.

Comment corriger le problème ? Pour le premier, il faut savoir si cette selection est vraiment importante. Qu’à-t-on besoin de faire sur tout l’arbre DOM ?
Pour le second, il suffit de mettre en cache la taille du tableau, par exemple comme ceci :

  1. var elts = target.getElementsByTagName('*');
  2. for (var i = 0, len = elts.length; i &lt; len; i++) {
  3. }

Et dans ces conditions, le recalcul ne sera pas refait pour chaque itération.

Conclusion :
De plus en plus, les sites sont remplis de JavaScript. Ces améliorations peuvent sembler superflues, mais c’est ce qui peut faire la différence entre deux site.
Si vous n’en êtes pas convaincus, pensez aux utilisateurs de netbook, de tablettes ou de mobile : Vous allez leur faire gagner un temps fou !