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 courent 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 strict minimum pour un gain de performance, avec un exemple d'optimisation de CSS 30 fois plus légères !
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 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 Dictators), 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 personnalisé
  certains comportements. 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. Dans un exemple simple, ce n'est pas génant. Mais c'est pour illustrer l'effet dans des structures plus complètes. Fort heureusement, ce point ci n'est qu'un détail.
  • 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 118 ko après compression. Pour le dire autrement, l'information utile pèse environs 0.5% de l'information total.

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

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

/* Inclusions Bootstrap nécessaires 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';

/* Inclusions Bootstrap nécessaires 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`. L'avantage ici, 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 fonctionnalité 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ée 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 fonctionnalité :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; }
    

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 CSS personnalisées Mobile First.

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


/* ... */

.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 octets à un code de 16038 octets. 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. C'est LA 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 ! Nous pourrions également ajouter des classes types BEM pour décrire structurellement le HTML.

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 octets 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. Tout cela pour une sortie de 4 ko totalement identique avant l'inclusion fonctionnalité par fonctionnalité. 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 10 000 appels (de différents clients) :

  • Méthode standard : 657 x 10000 = 6 570 Mo, 116.972 x 10000 = 1 169 720 Mo

  • Méthode optimisée : 431 x 10000 = 4 310 Mo, 4.018 x 10000 = 40 180 Mo

    Soit une économie de trafic :

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

Côté client, pour 100 appels d'un seul ordinateur sans cache (c.-à-d. à travers divers sites utilisant mal Bootstrap) :

  • Méthode standard : 657 x 100 = 65 700 octets, 116 972 x 100 = 11 697 200 octets

  • Méthode optimisée : 431 x 100 = 43 100 octets, 4 018 x 100 = 401 800 octets

    Soit une économie de trafic :

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

Côté client, pour 100 appels d'un seul ordinateur avec cache (c.-à-d. en visitant plusieurs fois la même page) :

  • Méthode standard : 657 x 100 = 65 700 octets, 116 972 x 1 = 116 972 octet

  • Méthode optimisée : 431 x 100 = 43 100 octets, 4 018 x 1 = 4 018 octet

    Soit une économie de trafic :

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

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 PreprocessorAtlas.

Lire dans une autre langue