Bootstrap, lisibilité, propreté, performance, optimisation ; c'est possible !

On me dit souvent que je n'aime pas Bootstrap, et pour cause, je pense que c'est une regression pour un travail Front-end de qualité. En réalité, ce n'est pas le Framework le fautif, mais la façon dont il est utilisé. Les exemples de mauvaises utilisations sont légions sur le net et les mauvaises intégrations HTML cour les rues.

Suis-je donc entrain de dire qu'il y a une bonne et une mauvaise façon d'utiliser Bootstrap ? C'est exactement ce que je suis entrain de dire, et je vais vous expliquer à travers ce billet le cheminement qui va vous conduire à :

  • Séparer le fond et la forme, pour un gain de lisibilité et de propreté,
  • Inclure et générer le stricte minimum pour un gain de performance, avec un exemple d'optimisation de CSS 30 fois plus légère.
Bootstrap et Less
Bootstrap et Less, themightycribb.com

Non, je suis sérieux, ce billet n'est pas une blague, c'est cadeau, et c'est pour vous.

À ne vraiment pas faire, ou l'utilisation Standard de Bootstrap

Pour commencer notre réflexion, partons de l'utilisation « standard » de Bootstrap. C'est la majorité du code que nous pourrons trouver à travers le net quand il s'agit de Bootstrap. Si vous êtes farouchement attaché à Bootstrap, ou que vous ne pouvez pas faire autrement que de l'utiliser (certaines personnes ont malheureusement des IT Dictator), oubliez dès aujourd'hui cette méthode de travail.

Voici un petit affichage de grille gratté à la va vite.

HTML

La feuille CSS Bootstrap est inclue

<!-- Composant -->
<div class="container">
    <!-- En-tête -->
    <div class="row">
        <div class="col-xs-12 col-md-6 col-md-push-6 text-right">
            <div>
                <h1>Je suis un titre</h1>
            </div>
        </div>
        <div class="col-xs-12 col-md-6 col-md-pull-6">
            <div>Je suis un petit texte explicatif à propos du site.</div>
        </div>
    </div>
    <!-- Liste d'élément -->
    <div class="row">
        <div class="col-xs-12 col-sm-6 col-md-4">
            <div>Je suis un bloc avec contenu</div>
        </div>
        <div class="col-xs-12 col-sm-6 col-md-4">
            <div>Je suis un bloc avec contenu</div>
        </div>
        <div class="col-xs-12 col-sm-6 col-md-4">
            <div>Je suis un bloc avec contenu</div>
        </div>
        <div class="col-xs-12 col-sm-6 col-md-4">
            <div>Je suis un bloc avec contenu</div>
        </div>
    </div>
</div>

Ajoutons maintenant notre propre surcharge pour faire un rendu de tout ça :

CSS

/* Nous colorons la zone de site. */
.container {
    background-color: #f2f2f2;
    padding-bottom: 16px;
}

/* Nous colorons chaque colonne sans sa marge, 
    nous avons donc été obligé de rajouter une 
    div inutile autour. */
.row > div > div {
    background-color: #ddddff;
    margin-top: 16px;
    padding: 10px;
    font-size: 1.5rem;
}

/* Nous devons tout de même gérer de manière custom
    certain comportement, ici je veux que les colonnes
    aient une hauteur fixe à partir de la version
    tablette. */
@media (min-width: 768px) {
  .row > div > div {
    height: 40px;
  }
}

/* Avec Bootstrap, soit le texte est à gauche, soit 
    il est à droite. On ne peut pas changer
    se compontement de façon responsive.
    On va donc surcharger la classe « text-right »
    qui dans notre cas ne veux plus dire grand chose... */
.row:first-child .text-right {
    text-align: left;
}

/* Je redéfini ici qu'il s'aligne à droite
    à partir de la tablette. */
@media (min-width: 992px) {
  .row:first-child .text-right {
    text-align: right;
  }
}

h1 {
    margin: 0;
    font-size: 3rem;
    margin-top: -6px;
}

Résultat

Mauvaise pratique : On constate au rayon des mauvaises choses :

  • Un DOM pollué par un surplus de balises, dans notre exemple nous avons une <div> inutile dans chaque colonne.
  • Un DOM pollué par un surplus de classe, non seulement elles décrivent visuellement et non sémantiquement la structure, mais en plus elles ne traduisent pas toujours le comportement réel du visuel (exemple avec la classe text-right surchargée dans notre exemple).
  • Une CSS Bootstrap complète et donc excessivement lourde en poids avec en plus notre propre surcharge, actuellement la portion HTML fait 657 octets après compression et le fichier CSS fait 118ko après compression.

Un début de bonne pratique, ou l'utilisation de Bootstrap avec Less

Voyons à présent comment nous pouvons nous en sortir en utilisant une méthode CSS-Driven. L'avantage de cette méthode va être de déporter le poids du fichier HTML à l'intérieur des fichiers CSS. C'est une excellente chose puisque les fichiers CSS peuvent être mis en cache, eux. Grâce à la version Less de Bootstrap, nous sommes en mesure de n'inclure que les parties nécessaires à notre habillage. Pour commencer, on arrête avec les affreuses classes Bootstrap partout dans le HTML, et on nomme les classes sémantiquement !

HTML

<div class="component">
    <div class="header">
        <div class="title">
            <div>
                <h1>Je suis un titre</h1>
            </div>
        </div>
        <div class="quote">
            <div>Je suis un petit texte explicatif à propos du site.</div>
        </div>
    </div>
    <div class="list">
        <div class="item">
            <div>Je suis un bloc avec contenu</div>
        </div>
        <div class="item">
            <div>Je suis un bloc avec contenu</div>
        </div>
        <div class="item">
            <div>Je suis un bloc avec contenu</div>
        </div>
        <div class="item">
            <div>Je suis un bloc avec contenu</div>
        </div>
    </div>
</div>

On ajoute ensuite les bribes Bootstrap qui nous sont nécessaires, et nous habillons la structure en Less :

Less

/* Inclusion Bootstrap nécessaire pour la page */
@import 'bootstrap/normalize';
@import 'bootstrap/variables';
@import 'bootstrap/utilities';

/*  Inclusion Bootstrap nécessaire pour la grille */
@import 'bootstrap/grid';
@import 'bootstrap/mixins/hide-text';
@import 'bootstrap/mixins/center-block';
@import 'bootstrap/mixins/clearfix';
@import 'bootstrap/mixins/grid';
@import 'bootstrap/mixins/grid-framework';

/*  Inclusion Bootstrap nécessaire pour l'alignement */
@import 'bootstrap/type';
@import 'bootstrap/mixins/text-overflow.less';
@import 'bootstrap/mixins/text-emphasis.less';
@import 'bootstrap/mixins/background-variant.less';

/* On se permet de recréer nous même quelques
    comportements nécessitant trop d'inclusion de code. */
html {
    font-size: 62.5%;
}
* {
    -webkit-box-sizing: border-box;
       -moz-box-sizing: border-box;
            box-sizing: border-box;
}

/* On travaille nos classes en empilement. */
.component {
    /* Ceci est la même chose que `<div class="container">` 
        appliqué à `.component`. */
    .container;
    .clearfix;

    .list,
    .header {
        /* Ceci est la même chose que `<div class="row">` 
            appliqué à `.header` et `.list`. */
        .row;
    }

    .header {    
        .quote,
        .title {
            /* Ceci est l'équivalent de `<div class="col-xs-12">` 
                appliqué à `.title` et `.quote`. */
            .make-xs-column(12);
            /* Ceci est l'équivalent de `<div class="col-md-6">` 
                appliqué à `.title` et `.quote`. */
            .make-md-column(6);
        }

        .title {
            /* Ceci est l'équivalent de `<div class="col-md-push-6">` 
                appliqué à `.title`. */
            .make-md-column-push(6);

            /* Ceci est l'équivalent de `<div class="text-left">` 
                appliqué à `.title`, mais l'avantage, c'est qu'il peut être écrasé facilement. */
            .text-left;

            @media (min-width: @screen-sm-min) {
                /* Ceci est l'équivalent de `<div class="text-right">` 
                    appliqué à `.title` uniquement à partir de la tablette.
                    Ceci n'est pas faisable avec une approche classique
                    avec Bootstrap dans le DOM. */
                .text-right;
            }
        }

        .quote {
            /* Ceci est l'équivalent de `<div class="col-md-pull-6">` 
                appliqué à `.quote`. */
            .make-md-column-pull(6);
        }
    }

    .list {
        .item {
            /* Ceci est l'équivalent de `<div class="col-xs-12">` 
                appliqué à `.item`. */
            .make-xs-column(12);
            /* Ceci est l'équivalent de `<div class="col-sm-6">` 
                appliqué à `.item`. */
            .make-sm-column(6);
            /* Ceci est l'équivalent de `<div class="col-md-4">` 
                appliqué à `.item`. */
            .make-md-column(4);
        }
    }

    /* Par ici on fait un peu de Less standard */
    .title,
    .quote,
    .item {
        div {
            background-color: #ddddff;
            margin-top: 16px;
            padding: 10px;
            font-size: 1.5rem;

            @media (min-width: @screen-xs-min) {
                height: 40px;
            }
        }
    }

    h1 {
        margin: 0;
        font-size: 3rem;
        margin-top: -6px;
    }

    background-color: #f2f2f2;
    padding-bottom: 16px;
}

Résultat

Avec cette technique, on a une sortie CSS qui a été divisée par 7 :

  • Le DOM ne contient plus la CSS Bootstrap complète mais uniquement les classes contenues dans les fichiers inclus avec @include. Actuellement le fichier HTML fait 504 octets après compression et le fichier CSS fait 16 ko après compression.
  • Mais ce qui est agréable c'est que Le DOM n'est plus pollué par un surplus de classe, elles décrivent maintenant sémantiquement la structure !
  • Cependant le DOM est toujours pollué par des balises inutiles.

Utiliser la feature Less :extend()

En Less, il y a deux approches pour utiliser des raccourcis de classe afin de ne pas écrire de code de manière redondante. Tout d'abord, je créé une classe modèle

.model { a; b; c; d; e; f; g; h; i; j; }
  • La première approche est celle utilisée dans notre exemple précédent, pour l'appliquer j'utilise le code suivant :

      .ex-1, .ex-2, .ex-3, .ex-4, .ex-5, .ex-6, .ex-7, .ex-8, .ex-9, .ex-0 { 
          .model;
      }
    

    ce qui génère en sortie CSS :

      .model { a; b; c; d; e; f; g; h; i; j; }
    
      .ex-1 { a; b; c; d; e; f; g; h; i; j; }
      .ex-2 { a; b; c; d; e; f; g; h; i; j; }
      .ex-3 { a; b; c; d; e; f; g; h; i; j; }
      .ex-4 { a; b; c; d; e; f; g; h; i; j; }
      .ex-5 { a; b; c; d; e; f; g; h; i; j; }
      .ex-6 { a; b; c; d; e; f; g; h; i; j; }
      .ex-7 { a; b; c; d; e; f; g; h; i; j; }
      .ex-8 { a; b; c; d; e; f; g; h; i; j; }
      .ex-9 { a; b; c; d; e; f; g; h; i; j; }
      .ex-0 { a; b; c; d; e; f; g; h; i; j; }
    
  • La seconde approche qui est notre fameuse feature :extend() permet de gagner de la place en sortie

      .ex-1, .ex-2, .ex-3, .ex-4, .ex-5, .ex-6, .ex-7, .ex-8, .ex-9, .ex-0 { 
          &:extend(.model);
      }
    

    ce qui génère en sortie CSS :

      .ex-1, .ex-2, .ex-3, .ex-4, .ex-5, .ex-6, .ex-7, .ex-8, .ex-9, .ex-0,
      .model { a; b; c; d; e; f; g; h; i; j; k; l; m; n; o; p; q; r; s; t; u; v; w; x; y; z; }
    

L'un des problèmes de cette seconde approche est qu'elle ne fonctionne pas avec des fonctions. Ainsi la fonction Bootstrap .make-xs-column(12) par exemple n'est pas utilisable :

.ex-1, .ex-2, .ex-3, .ex-4, .ex-5, .ex-6, .ex-7, .ex-8, .ex-9, .ex-0 { 
    &:extend(.make-xs-column(12)); /* Ce code plante. */
}

Un autre problème est que :extend() n'est pas utilisable dans les Media Queries. C'est un formidable atout qui est très limité avec l’utilisation de Bootstrap ou des approches Desktop First mais qui peut se révéler très intéressant sur des grosses feuilles custom Mobile First.

Voyons rapidement en quoi cela changerait notre précédente feuille :


/* ... */

.component {
    /* Ici, `:extend()` est appliquable. */
    &:extend(.container, .clearfix all);

    .list,
    .header {
        /* Ici, `:extend()` est appliquable. */
        &:extend(.row);
    }

    .header {    
        .quote,
        .title {
            /* Ici, `:extend()` n'est pas appliquable
                car ce sont des fonctions. */
            .make-xs-column(12);
            .make-md-column(6);
        }

        .title {
            /* Ici, `:extend()` n'est pas appliquable
                car c'est une fonction */
            .make-md-column-push(6);

            /* Ici, `:extend()` est appliquable. */
            &:extend(.text-left);

            @media (min-width: @screen-sm-min) {
                /* Ici, `:extend()` n'est pas appliquable
                    car on est dans une Media Query. */
                .text-right;
            }
        }

        .quote {
            /* Ici, `:extend()` n'est pas appliquable
                car c'est une fonction */
            .make-md-column-pull(6);
        }
    }

    .list {
        .item {
            /* Ici, `:extend()` n'est pas appliquable
                car ce sont des fonctions. */
            .make-xs-column(12);
            .make-sm-column(6);
            .make-md-column(4);
        }
    }

    /* ... */
}

Bonne pratique : Avec cette technique, on peut donc encore réduire la taille en sortie même si dans notre exemple nous passons seulement d'un code de 16277 bytes à un code de 16038 bytes. Dans de nombreux cas l'écart peut cependant être significatif !

LA bonne pratique, ou l'utilisation de Bootstrap avec Less et par référence

C'est par ici qu'on touche réellement à un point intéressant, et à la réelle chose qu'il faut faire quand on utilise du Less, et surtout Bootstrap ; l'inclusion par référence ! Avant d'en parler, adaptons rapidement notre HTML pour le rendre encore plus sémantique. Cela n'a rien à voir avec notre propos, mais des <div> partouts, ça me donne des boutons !

HTML

<section class="component">
    <header>
        <div class="title">
            <div>
                <h1>Je suis un titre</h1>
            </div>
        </div>
        <aside>
            <div>Je suis un petit texte explicatif à propos du site.</div>
        </aside>
    </header>
    <ul class="list">
        <li>
            <div>Je suis un bloc avec contenu</div>
        </li>
        <li>
            <div>Je suis un bloc avec contenu</div>
        </li>
        <li>
            <div>Je suis un bloc avec contenu</div>
        </li>
        <li>
            <div>Je suis un bloc avec contenu</div>
        </li>
    </ul>
</section>

Le réel intérêt va résider dans l'utilisation de l'inclusion par référence. C'est à dire qu'au lieu d'inclure les portions de Bootstrap utile avec @import, nous allons le faire avec @import (reference).

Less

@import 'bootstrap/normalize';

/* Inclusion des fichiers, non plus entièrement, mais par
    référence avec « (reference) ». */
@import (reference) 'bootstrap/variables';
@import (reference) 'bootstrap/utilities';
@import (reference) 'bootstrap/grid';
@import (reference) 'bootstrap/mixins/hide-text';
@import (reference) 'bootstrap/mixins/center-block';
@import (reference) 'bootstrap/mixins/clearfix';
@import (reference) 'bootstrap/mixins/grid';
@import (reference) 'bootstrap/mixins/grid-framework';
@import (reference) 'bootstrap/type';
@import (reference) 'bootstrap/mixins/text-overflow.less';
@import (reference) 'bootstrap/mixins/text-emphasis.less';
@import (reference) 'bootstrap/mixins/background-variant.less';

html {
    font-size: 62.5%;
}
* {
    -webkit-box-sizing: border-box;
       -moz-box-sizing: border-box;
            box-sizing: border-box;
}

/* On permet des listes au rendu vide. */
.ul-reset {
    margin-left: -15px;
    margin-right: -15px;
    padding: 0;
    list-style-type: none;
}

.component {
    &:extend(.container);
    &:extend(.clearfix all);

    .list,
    header {
        &:extend(.row);
    }

    header {    
        aside,
        .title {
            .make-xs-column(12);
            .make-md-column(6);
        }

        .title {
            .make-md-column-push(6);

            &:extend(.text-left);

            @media (min-width: @screen-sm-min) {
                .text-right;
            }
        }

        aside {
            .make-md-column-pull(6);
        }
    }

    .list {

        /* On applique à `.list` un reset. */
        &:extend(.ul-reset);

        li {
            .make-xs-column(12);
            .make-sm-column(6);
            .make-md-column(4);
        }
    }

    .title,
    aside,
    .list li {
        div {
            background-color: #ddddff;
            margin-top: 16px;
            padding: 10px;
            font-size: 1.5rem;

            @media (min-width: @screen-xs-min) {
                height: 40px;
            }
        }
    }

    h1 {
        margin: 0;
        font-size: 3rem;
        margin-top: -6px;
    }

    background-color: #f2f2f2;
    padding-bottom: 16px;
}

Résultat

Bonne pratique Et là, c'est le jackpot, on a une sortie divisée par 30 !

Ce qu'il se passe est que toutes les classes et fonctions des fichiers inclus par référence ne sont générées que si elles sont appelées en tant que raccourci dans le fichier qui les appels par référence.

Ainsi pour la sortie nous avons maintenant un HTML de 431 bytes après compression et un fichier CSS de 4 ko après compression !

Ne plus se soucier des inclusions

Avec un appel par référence, comme vous l'aurez peut-être deviné, on peut même remplacer l'intégralité des appels suivant :

@import (reference) 'bootstrap/variables';
@import (reference) 'bootstrap/utilities';
@import (reference) 'bootstrap/grid';
@import (reference) 'bootstrap/mixins/hide-text';
@import (reference) 'bootstrap/mixins/center-block';
@import (reference) 'bootstrap/mixins/clearfix';
@import (reference) 'bootstrap/mixins/grid';
@import (reference) 'bootstrap/mixins/grid-framework';
@import (reference) 'bootstrap/type';
@import (reference) 'bootstrap/mixins/text-overflow.less';
@import (reference) 'bootstrap/mixins/text-emphasis.less';
@import (reference) 'bootstrap/mixins/background-variant.less';

uniquement par :

@import (reference) 'bootstrap/bootstrap';

et c'est la référence qui fera le reste, le tout pour une sortie de 4 ko totalement identique avant l'inclusion feature par feature. On est bien loin de nos 118 ko initiales !

Petits calculs

Nous allons voir rapidement la quantité de bande passante sauvée en aillant fait l'effort d'ajouter (reference) et :extend(). Gardez en tête que ce n'est pas pour une page complète, mais bien le petit fragment d'exemple que nous venons d'étudier.

Nous avons donc :

  • Méthode Standard, avec DOM négligé : HTML = 657 octets, CSS = 116.972 octets.
  • Méthode Optimisée, avec DOM propre : HTML = 431 octets, CSS = 4.018 octets.

Côté serveur, pour 10000 appels (de différent client) :

  • Méthode Standard : 657 x 10000 = 6.570Mo, 116.972 x 10000 = 1.169.720Mo
  • Méthode Optimisé : 431 x 10000 = 4.310Mo, 4.018 x 10000 = 40.180Mo

    Soit une économie de trafic :

    • HTML = 6.570Mo - 4.310Mo = 2.260Mo
    • CSS = 1.169.720Mo - 40.180Mo = 1.129Go

Côté client, pour 100 appels d'un seul ordinateur sans cache :

  • Méthode Standard : 657 x 100 = 65.700o, 116.972 x 100 = 11.697.200o
  • Méthode Optimisé : 431 x 100 = 43.100o, 4.018 x 100 = 401.800o

    Soit une économie de trafic :

    • HTML = 65ko - 43ko = 22ko
    • CSS = 11.697ko - 401ko = 11Mo

Côté client, pour 100 appels d'un seul ordinateur avec cache :

  • Méthode Standard : 657 x 100 = 65.700o, 116.972 x 1 = 116.972o
  • Méthode Optimisé : 431 x 100 = 43.100o, 4.018 x 1 = 4.018o

    Soit une économie de trafic :

    • HTML = 65ko - 43ko = 22ko
    • CSS = 117ko - 4ko = 113ko

En Bonus

Pour développer des sites web avec Less en toute transparence, je ne saurais que trop vous conseiller le module NodeAtlas en node.js. Cela vous permettra de développer dans vos feuilles Less et d'appeler les résultats CSS. Un exemple d'implémentation est à votre disposition dans le projet LessAtlas.